From d55859c593750e04cf94d518b33a50eac3e29470 Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Fri, 10 Apr 2026 23:59:23 +0200 Subject: [PATCH] Version 1 --- linedance-app/app_logger.py | 33 + linedance-app/build_windows.spec | 2 +- .../__pycache__/__init__.cpython-312.pyc | Bin 1028 -> 988 bytes .../__pycache__/local_db.cpython-312.pyc | Bin 27731 -> 29709 bytes linedance-app/local/local_db.py | 208 ++++-- .../player/__pycache__/player.cpython-312.pyc | Bin 10510 -> 10987 bytes linedance-app/player/player.py | 27 +- .../__pycache__/library_panel.cpython-312.pyc | Bin 18441 -> 24159 bytes .../__pycache__/main_window.cpython-312.pyc | Bin 57420 -> 58629 bytes .../playlist_panel.cpython-312.pyc | Bin 31112 -> 32095 bytes .../settings_dialog.cpython-312.pyc | Bin 15958 -> 17360 bytes .../ui/__pycache__/tag_editor.cpython-312.pyc | Bin 26437 -> 21946 bytes linedance-app/ui/library_panel.py | 108 ++- linedance-app/ui/main_window.py | 72 +- linedance-app/ui/playlist_panel.py | 27 +- linedance-app/ui/settings_dialog.py | 69 +- linedance-app/ui/tag_editor.py | 687 +++++++++--------- 17 files changed, 743 insertions(+), 490 deletions(-) create mode 100644 linedance-app/app_logger.py diff --git a/linedance-app/app_logger.py b/linedance-app/app_logger.py new file mode 100644 index 00000000..a1249700 --- /dev/null +++ b/linedance-app/app_logger.py @@ -0,0 +1,33 @@ +""" +app_logger.py β€” Central logging til fil i stedet for konsol. +P₯ 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") diff --git a/linedance-app/build_windows.spec b/linedance-app/build_windows.spec index d98dd1d2..e56deb62 100644 --- a/linedance-app/build_windows.spec +++ b/linedance-app/build_windows.spec @@ -64,7 +64,7 @@ exe = EXE( bootloader_ignore_signals=False, strip=False, upx=False, # UPX kan give problemer med PyQt6 DLL-filer - console=True, # Vis fejlbeskeder + console=False, # Ingen konsol-vindue disable_windowed_traceback=False, target_arch=None, codesign_identity=None, diff --git a/linedance-app/local/__pycache__/__init__.cpython-312.pyc b/linedance-app/local/__pycache__/__init__.cpython-312.pyc index b47985a599847c1384ea53483a1d3209abbc691a..e37789129a4a878433b76c954dcfa0c0a2ca2196 100644 GIT binary patch delta 25 gcmZqSxWm4ogPB)HKO;XkRX;f=u{0%h@?K_l0CA)U^8f$< delta 65 zcmcb^-omk=gV{7hKO;XkRX;hgsJJ9GPv0d!yEHd7uOzid-#xR$IVV3aRo^EwFEu4G UFFDmQt+*gFCnvRN@>FJb03SaXf&c&j diff --git a/linedance-app/local/__pycache__/local_db.cpython-312.pyc b/linedance-app/local/__pycache__/local_db.cpython-312.pyc index 17c6bce07f8d48a244ce93d36fdda66215a75428..17f7ce47bcfada0f36b303e10c408ade765d16b3 100644 GIT binary patch delta 6516 zcmb6-dvFxzef#a>9(q4dLI`PvB;bUQK!Ai02+5Lo3PB_=!3jB5x2xmOX;1lfPeNE! z!o_1`R3mxY0>zHOsV6Xy*2!fOPuip=yzN ziA*xxoBQqe_`SaG_xOI_{%o53$*)M-Pf}Bp7YAp zl~NUQywJE>mp#IsbTW*RF1xk>w(Q1nq+9s(uyWEF{)LR+GoK9enF=qboMCRv62Fl3 zd+<{MKU+>)eujA=M^0CA0oBvP%Na17C%4F%N6fS(H-@}OTV=R=#7mq1&@wx!4hTyU|qV8APYxe~cmUJA2?iYHN8 zwnJV9(w3uy#PMysTm)35v*SfdE=pMf%LRVLFjFSC$s+J8nO%=VUJiW96L>5cDFwV0 z@^-llc$Wj-3V2&O0hXu7C9edEmGUYW^;+wx0Q^-Jwp%FzIjf=Xl`CO>4eU?utn?}v ztpE~at(;Cl6P85SZ@j603+4%MqpWF z31a11V6!%HL8WytvQFNVfKv}6HS%UqF~Nj8c)0<_*Si@vBk!_iw}1{?=a%0HJ&-DI zgFl)wO>P3|8{}r(WcfLu-8jU0cSkRB^&~aL7YK#BrI61bR#oE;pSx55Gk}XTGnE7# zU7vMAh8Z0*);fxe?{wrCJwoOlcYNZw6(I4R{nk_CeVvWBZU8@gWL_pWg&QaX9*NOi z#tC6rvE2pebH$tUvX5t@&l&$EB&R$xnC?h!ogeICysne$I~6@bRfQ^4R8-MkqSzCr z-9c4WLYg85MJb|({o%g8pf2`>Wu>aaIPJ(UXU$CiF~#5O)1*Gdryrw=B%A3$EgX{c zU|98qlmR7VeABU{@Tq5iXY-D|&B#^>22@|KGNf(Y*Wpc~E@Q?~&1pI{L}y_RGlT1S zT3LN8+^1CgLsCRmj6ZT#=3#=!gTHnhz$o+B!MJknIF}k9IP>Maq6;IFBY&ygto!=# z`;qCohQ}Pq-+DIXMPr2t3AGmVvy9%dVg?ExG+nAPXoU+Ep<4!jtrmb~z?AIs z+p5R5y|Mm{!OL6T+juXzd9?Kb*z(267e`y~@j27H>y`G??RPT0_xOtGLkJ~UqN>BoQuJ-p-FCvS^rLlX5%d!`(GgH|pWIDLVSEkFRRH)E z^8|MBfQw1VJ=ZYW8e=&jZ8|OMT+)*V$vKZ00zFr1jDz?8XlS;Vl1s>Id^YnsDIrF3 z5%)E&ge>rNbY9FeGah5zm-AY?z@8o81E?IuJZwdWZPg@OAs>|n=?a+I1b+=P+b9FV z&o$iPJr=gH?+YGwoEQ(wJ;oJx89U4v4-e*wY&>WP6JTWSZE}hXlUC*ksCkM7a~kj0 zVjKX3aR56+ev ztw{D}3RVO~y9!To^-kN?smck(AJG-fPlNrs6+Cn~sAjr)6y1L;tSY9^PlKv%av??a zI%u(xFP4v(91Ra@C=%maoDDH&Pz_q{u^A?|z+d|Wz$o)ES^^xy#R%^Eyl0B{#27!x zcRdCuxMS@cjL*l{BZo7~F}gdJ!DJLXVt66*EC(*-&YVukINv_jelh!>Q(Vhp0!+lx zjQ7N<@|;3IbElncnZ=mS@K0xC|3=_a(_$PKW5zO@kD9KSIBisp!x)kD#K_HkkS-`i1gw zQa8utI!iI{JWHEKmA>$RGN;0B!1~W<&dSSV#~iVjvx(d1IB?K{HQ4c#AL*ANk;GZ} zG{2vi?Ncl zaj9lwp@bsQxxA@#KeoFO_z@Hvzpkm->ci$y04DFFO2A~*J`EE;XgB}ZdL~EuHHGRv zEvyD81)QhR!|Q*+l1}4?J!8h-)^6Ci69!?YI^p%~PG$B<6p95P_Vood4bmcw0OFo# zd0ke*PFstiSfwnmQ3l$`BJLjYp##wqfYKRNHx+VCEzksSISDWNA^_6~7?6!sTZ@Gr zlcFgS^&hjCX>-a)Z0p?wI(5m-846xxa`PDVKxjFyq~urDr=o(KHN=r3D8B9(JOU}Y1? zW)grY3`ik}d1Rirw$HYg2$x{&YBXoZOWa}wTai2nz$@5VTDo1v(Ki5?MANWfvz*xW z-#oNrTr1yTP>=h-*8uNk^f$Y&u=ReVpVQJVc4Qa>ec!^VD+ny4#oB(j*CV_QvpsRWEnMF=P>+B)2`;6;mQXEZo z4U+=n`p#ts$E)oU(3^bn&4k;I3KV42#tXQ9s`La;Kp`%PaBe7KucFGj zB5L6X^($H>mVm~UuENdYF10rV)ihq7Y)(jG1psgdR4mY~mQM;{K7yOkfTC5Myk`8c zE8lqc`9kB9t_W1IU|b&n(;d+iD3~LvesTh(%spLg?QJaw#MWJ12X=Kp!)B_&%}bOt zkPYwco%{AC%3eT!XlGkzTOw8au03tyHnC-2d%KZzAkWxyAhRCN#FK9=pj#{#mf?F= zw$TyDU$%hGmN$=WJndQuGBVb5_`xUV9u8V~wU}88;LpefW{eCo=#a@!`5h-{+L0c% zi=pK(<|P-p_sjpj>8c$YYtj{az(;@-5Jz;&VRc#4{ z{ZdG4gocgnf`w~%hj}u}Tq(asuCBVW#;!ZPV(qA1{epZBZmy7TN&(l- z(wK!zhZY9ckxd@&wO$v!iPLxv+4=I15rd+2<~%e9hIJU*8>r zFC%HOBql5G5yQI6&o)n|XH0m;hOP*A)61tz%O->g>7rvYcsE0g34nm8o(qMOBZa`c z_;)eSTF5*dDf0gy^M}a%^Np~;$Bj%%)E3s`Svl#(7I(hK`@Ne*L_IFoQ(ii2dqQQTa}X?3f3nXjot>d8l&aNR`P1}^pO}lV(I-d5>r zQ0d1wvS5x&~^BS3glKDHeJE0FNo`To2E6$p|Sj=$% z2KNB-m~kzzrEvgB$pJ6<)bd+m8E!U9eu@`GRoN0z^Qw_iP2W@Xl#(<~!JG0$}pp;c&?0 zvDUP|5VheHUW=^jj8(GX?O}wm%73ejx2kZfq^4&O5xvB!Q?1&OUcxRGY4mLXrl3dq z;iDE-M-=NFiW$=mOAHFjy)BQkJj2>`Yv&ez$0%mtxPZ@@k&x0z{{!d<{I%5_+_*SG z$Ri^V+#Hy$s2WW<8@Qh)PNj)gHcX|h{25afb0m@ciMspQIqM=7`AcTcTdzE+b$`jrxf=d{B-fu zd05!xQ3RTGn7cT=xhsdd4s*M5X||F_B_+R0pl;wQpe@v+6ar-yNUPNs^CfLLhs9k* zG#7Yyng=|^(|A7MCA2^(1@1gJUkK|mIty@~dI2v-9QIJo!HorT_+baf0bdd9&8Dri z71n%iB`dxN?Hxn`IbKmfWOM(mXuP^R0Hd3>WBS> z5S_ef{fmG)A0#YL^3znywxn!`MJ;gDbkI5ox1YAt#TjlVh1h95s9s3dSx!7+DPIDV zMRX~MEVUHzbQy@L&2*`nr^|s>M;kJ@Rsdx&eH4t$Na4E2fLib5oE%+m?KOf8E7R_; zf)!|`tKlD0>7i>t`y;do!%Umu?2;kgzro;zM$%?yiq^-Xa%PbUuz)J$?mbJO7C8-I z@}u!7jFOAfILp-s#R}oTe$B35lFIoW7gmRUQOuRyDefLm3Y+=X>?xcIuZbyX`#3E- zMeoT?VKd(&T)>%Hjc`4)P*;xq;N*pTT^1{JwaQ!WsOGeJeCd|&LS?~X;3dO65cO^MdG$9nrBN>o!={f3a%7LG-g z`YoZTLW5CRY3)+up>SAX`Z-7KC=L%V3Q_?a;Qr*we{sdh6_c)cKj`{V?<>96<~QDw zawa@uo*Pp6b*cR29hcg#N#&DL=*Me|JoXVG)ju;~({kX5^`QKrHOeV`cUEHe7LUD~@^jHOgIRH$lOVQ+RsAbbN%@+wqhfFcfLQ&1M zg_WpZgtj?8uXSPcqyFRBnX!|D&l{zU_J@|gbllKW(>@wcTz zgjDLG<)`MP#aLl5McNmMtELSa*0e!@^tlZU<DTV34`haokM|m5blOPAh#HwuPnEU=QC*{Tv`aTk7Hi3fou>mRqCA-nbcYd_sIlb1%=0qd<8 z9|GJmn&AY^kO3<*M}{j@Qv&gDa3~yt>JCkq+$5HfC_{U6k*4PQgIvzqef)I zZBm<-2TmQuW5)oPVt+6Uy=dLN8KLRlG*|o%>`xeHHXX99NuN!YK?@%50ssTbGRQLH zBvMZSFo~Kuu=L$qXN#wS{yk&g)|dGm0py#J*on)p0|2|uApQ~pi|HY&fNtihLFh}R zA6^3iRfDjzvp{~scA%7m$K2mn@J2fSH6nkd%Mz@|C{^ z^bZIWbT1%qdpL7mC{Az=(SLl$l)iHJlFd~$ALi7Y3?3)P=j+bj<<$Hm2R82@$3O3S zxLm)rwoZJZmV=BJ7PjWw-pp=w+piEF&?{~cVZPW}W}M$+5}1SspM9cfCi&BMfYoHL zgMg&H-GDvVWXzg{2tFdZCvetbis-&-yb(CfkK%FD*{3Q2Io21|7{0bTE=UY-O@=+e zHX_)B;Bf>q1_WX0x3fz~eTW^a&lEzM5>Yez3aKUPP*e_B&(Azg{2*yL~ zOZFgNb;IdY-aoG`>9v_8Bp!|DY}My%eX-ke5>WYHDk8HRtGZxf>Ceb>HlYx zE!-b|z$RVl_K9x;yT_>OmiZFY3s>PgD8OnW9x!VtT5;Nj=CKX*q!X5-y5LoGa4=_^<^!0_P zX-7S&S%W`la86mD=cT z(PHtpKwho?C%U=_e**Z)4nO(xQVU}kT{c_)N~|c?La*W)G5=D1FxD|@t?>zIX0^eO zA4SIZwe^%s`i-wGX2s$hVylr;=#0g}reF>JSx9X~upR-v0IV@--NlUI60b+=c4K&f zv4aTkzQ9{xlw&xr84g}{8o^lrrl|GB;pfeHB;f2}*tW@rz*=KiP7IUJFtJw97zW`2 z$I6g^bLb0cbtFdn!b%hS5S9e~)xYo@m#`5+?(h;3?iF(Sr}1Tr6Lu#lI{xU5g6iu9 z)%Q49B#Jpc_YUtMo_jXH5{TU`+{}}vM3-^+xgU}WC-J|mCV)OQ55 sqlite3.Connection: - """Returnerer en thread-lokal forbindelse.""" - if not hasattr(_local, "conn") or _local.conn is None: + """Returnerer en global forbindelse i autocommit mode.""" + global _global_conn + if _global_conn is None: DB_PATH.parent.mkdir(parents=True, exist_ok=True) - conn = sqlite3.connect(DB_PATH, check_same_thread=False) - conn.row_factory = sqlite3.Row - conn.execute("PRAGMA journal_mode=WAL") # bedre concurrent adgang - conn.execute("PRAGMA foreign_keys=ON") - _local.conn = conn - return _local.conn + _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.execute("PRAGMA foreign_keys=OFF") # FK checker forhindrer level_id gem + return conn @contextmanager def get_db(): + """Context manager der bruger app-forbindelsen i autocommit mode. + Hver statement committer med det samme β€” ingen eksplicit transaktion.""" conn = _get_conn() try: yield conn - conn.commit() except Exception: - conn.rollback() raise +def get_db_raw() -> sqlite3.Connection: + return _get_conn() + + def init_db(): """Opret alle tabeller hvis de ikke findes.""" 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(""" CREATE TABLE IF NOT EXISTS libraries ( 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); """) - # KΓΈr migrations for Γ¦ldre databaser (each separately) - migrations = [ - "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 + # executescript slΓ₯r foreign_keys fra β€” genaktiver + conn.execute("PRAGMA foreign_keys=ON") - # 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] if count == 0: defaults = [ @@ -174,14 +186,49 @@ def init_db(): (4, "Erfaren", "For dedikerede dansere"), (5, "Ekspert", "Konkurrenceniveau"), ] - conn.executemany( - "INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)", - defaults + for row in defaults: + conn.execute( + "INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)", + row + ) + + +# ── Versionsbaserede migrationer ────────────────────────────────────────────── +# 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,) ) - conn.commit() - print(f"Dans-niveauer seedet: {len(defaults)} niveauer") - else: - print(f"Dans-niveauer: {count} niveauer i databasen") + + + @@ -282,30 +329,49 @@ def upsert_song(song_data: dict) -> str: 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: - conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,)) - for i, dance in enumerate(song_data["dances"], start=1): - # dance kan vΓ¦re str eller dict med {name, level_id} + file_dances = [] + for dance in song_data["dances"]: if isinstance(dance, dict): - name = dance.get("name", "") - level_id = dance.get("level_id") + file_dances.append(dance.get("name", "")) 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"]: - nm = dance.get("name", dance) if isinstance(dance, dict) else dance - if nm: - _reg(nm) - except Exception: - pass + file_dances.append(dance) + 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 @@ -465,15 +531,41 @@ def clear_event_state(): # ── Dans-navne ordbog ───────────────────────────────────────────────────────── 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: + # Hent fra dance_names ordbog (primΓ¦r kilde) rows = conn.execute(""" - SELECT name FROM dance_names + SELECT name, use_count FROM dance_names WHERE name LIKE ? COLLATE NOCASE ORDER BY use_count DESC, name LIMIT ? """, (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"): diff --git a/linedance-app/player/__pycache__/player.cpython-312.pyc b/linedance-app/player/__pycache__/player.cpython-312.pyc index 0e5a562224c59b4fc5c5ae841ecd06d7f0bc1c39..9e97056f18123c667be476b15844f868627e33d8 100644 GIT binary patch delta 2976 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 0: secs_left = self._demo_stop_sec - cur - if secs_left <= FADE_SEC and secs_left > 0: - # Fade fra fuld volumen til 0 over FADE_SEC sekunder - fade_fraction = secs_left / FADE_SEC # 1.0 β†’ 0.0 - faded_vol = int(self._volume * fade_fraction) + if secs_left <= self._demo_fade_sec and secs_left > 0: + fade_fraction = secs_left / self._demo_fade_sec # 1.0 β†’ 0.0 + log_fraction = math.log10(1 + fade_fraction * 9) / math.log10(10) + faded_vol = int(self._volume * log_fraction) self._media_player.audio_set_volume(max(0, faded_vol)) self._demo_fading = True elif not self._demo_fading: - # Ikke i fade-zone endnu β€” sΓΈrg for fuld volumen self._media_player.audio_set_volume(self._volume) # VU-meter: brug VLC's audio-amplitude hvis tilgΓ¦ngelig, ellers simulΓ©r diff --git a/linedance-app/ui/__pycache__/library_panel.cpython-312.pyc b/linedance-app/ui/__pycache__/library_panel.cpython-312.pyc index 43491e1d94a433b27593c501342eccd2f42ced25..a645a86a7933a2a22c677ca2f0677a651824c6aa 100644 GIT binary patch delta 7812 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 delta 3114 zcmZ8i4Nz3q6@K^a+g+CZW%uo}yC5w8E+PU2tgDGXMMWbdC~8nBuDmBKfrZ?C)JPY_ zgif@j(e^Z{3E0s#X>hco4(&`PJVARqQ@bBTa5s3_0Hl<4Z z5Vy%H84zt!0Nkk?D&+KSyyO`YHu)tH*#XHe3&`;zCn))llOd%eX71r$Ae&~b-^utL(v=`3aBMdt)X@diN z_U-)Il<|G`Qeqkql0v5kt~;{f6Q>UxPgw-?DZnRI5jrAX1J@aZ2Hz}*yYj)kMudS{ zYqpZDM}2gPNOG+%otK2OCLavCMVn>DE*O*K!9IwkTk1n(@-U31<~!tMIKn}_6F>_7YxEX9R$}|3=LutjHWqAv6k2w zYmw97%d{zTb@ihhY;=c7Ig5cf;Z-*#&m%%CB4Pq6wFI2<_~8q87Cf-KK@j~Gmy{;q ze^h{_Zco5HBZv&$DxPWeR1h{868!{cn96s>V%+VY#T*Qd@qKWjM9VsXv^8EBh*)jP z0^J5iworV+DeCsMf{17vx;RgZ5EQSMl>kPn@w1oeYafrwGM(K^XT`NQ-grF}s*Cpa%Jg$T&f&H6Uy(xCGt4{!T`nfqZk^l$|6+GO|@6(b2P=b|Oj38N?962Aad@ zAqL-qBcTOkCwv;3a8Ij+`ALnv@SB_(@-kR+U$WP+l~ep-7|E?9!*DLQe^xE>RbxVl zQiZ;Vbb|F;h~^{GIJD>GlkblY=ly~=J;ykEVP#=C>46=E%Sj!)SC~m&9lu&=CuDT| zR#6*qA0hOIs24V$x4>~PJE1$P8X}=|7+l3_I0e43K-9*LT5U9Z+nIzkgV$EW?I$fi<+uToByf06 zj)B)kFQ{{D2E!EnpEOGp%FLb)G{h zH6{&7Qvii=A{hr#l1A8F;GL5~%}MhJCz+BdBiunloFvWqD#8s{3tZAjMae9sbf)7K zJVr~($QII%%mA)^+wc@s;onr6l6^&K=6MST zl}L0(Ar4L!;@c?cFM?HH$%fHz%C6HmO|VQp{ET}z$ej;H#-azx50SUchs{5+j6OLL zjQo?1u{GvA`K!^gin6ee_TpSMwspx-N+(+e8I(OVT4&IejH+Nt7CGM6Ez_tX(>K`M z#BPkZqzauqvAAmN6W9d;^_9W(_Jpjg=%I2u?dgk4)Xhx%j4d%oC25r+cZXe*4L_er zjxji{iMwNoL@eH}TBT^bO>XVQCQqm&Mt_b{xLfIuoYd5|_I9@>6sj7#VsW{GRx<-y zcGx==`trAAGF2n+d{w~A5|+3^;Cofe3_eQ%d{k9oykp`rW`VW(rst|B_mF&O=Gi3`EViRz2N$i?)? z|EpYdRb8^V_O0H#`eR$xge~iU@{uk7?p$7bc&9d(P`FZ?%b(%K|65xl znBHf*>4jBwa@KA}{u)8`$WlycRif>QRw_T=CnpqHf>U*6{QDf-uInb-p}9Vv|9~5R zvHtJ;95uD8r!Crr`BOURY_^_{N%a(O1-w#Kb3)PG1)`sV>b%Df&d0QAwusGt{S_GPsGL3Q|v8rYuJNhSwC>5l!8flh?9ppndWF zGPvGQVk$-QJ_luu8$!33A@Q060Sq$?P$)utg9S5)IN{>I=QLLvoWi&k4prfO;eIsF0LSAhQuH=eff zJ(9l*S(swds?~RbWjr+5tdCL_bw#_kO3^3b?z0ud0{Kl5{viijnle3_d%V@B6N&tN zO@7h_V@-L)H2(Xh3bOD6lv9GQohr7=%JLUt2?blFCPw8prHAgI|3LmnK z*xJ+6r5dmU=^n=X4ujVjyuu*PfE@xoz+i-di2>y}H%GTKlBJHeF)+jS=AzIUBp(iO zm;JeiDlhnF57tdnPa`#AgVW7HYl<;&Ph4m$xVMwQ&E{u~0b{|P2&`&ZfQ1i3EmhLChWj733lP8n diff --git a/linedance-app/ui/__pycache__/main_window.cpython-312.pyc b/linedance-app/ui/__pycache__/main_window.cpython-312.pyc index 196baa3cc9566e935a74857f339230e6784d78fe..79e44ac2e4c687c3223660aef9d1222a5b502c63 100644 GIT binary patch delta 10506 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 VL~|~#VC!O7(yp}cH1sR_{4cJZ;U541 delta 9953 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 bbhUY>@HtSrc1C1P2y^bd{hC0(vf6(EH<5re delta 4110 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 b;XqGFtmR7}X~RNmzmd7OHGC=1e?{!S0znoD diff --git a/linedance-app/ui/__pycache__/settings_dialog.cpython-312.pyc b/linedance-app/ui/__pycache__/settings_dialog.cpython-312.pyc index c87a0a4da861f2d45cfceb805c98239d692f26e8..11666ca1c3fef09febfffb097967e665d1bb0f68 100644 GIT binary patch delta 4028 zcma)9eM}t372nz0J1_Ua*B!^_9lKx*3kLhlhrxWA#$Zg~Yztx%*UII5yWlg&9i82C zY>I=eq=_A+3Gt+r(|nmmwHsM#5+Nm2YSSuFB}dI4kRySftk#h#RhFt0;#5|uR_eUn zX*x%zN|Z?YmE>7M?IIv1~tD zB0Ch%2rFkk!5#I=Svs9f=~9;2&&WAXFmkTKUN?a{!yFaPI3?HHA3ub)7fnQhXO0I$ zV>@JJJQf%U%1S_0Lb0fmE25=;_+mopj#AKXS1Dfq`z4Y5sh=k?vnef9f2CbIeUXXQt-z z=>%VOuIrY^``Yk}!%G#r=e3U-E)OR>hgQ~Ed*<;+^}srqX0^`$;G3-G%~I|2rxKo? zv{Gm5JWhDNmmasx$G>H(jS_TcI$U#F?Uq%>N?x#3us2xpDqB4FspTDnGD*%BiM+!L zrYt!dXyhXQu#}o|WPJv?VKkj9XI^Ki!L+USh&jVZPCtg5#Q6YT3PZRQNbnFA0Ez%a zfMS3$fD(Xm01v=c3O%?2$ZZrPJ9g48$*YQXBp8V+xav5JO{0p2t3hfzKrKKWzz%?& z06u_S6eJe!2D+Yt3I`Vi+(5geLk*sSh}X1mbX3Kylvf2ZUy&Cx%BOF*5|V$3JGjD` zpXxi`x0JQ@m+m)<-Y9y@e`^)GP)>6r>+W}#vKjhuvZ{}R`qo+?Wz^=?wCOwA13 za3`cgOWfhKj=tP6y3_rel(wam`fubVq=QS`p|sNa>H0S5+&l9LH-MYwj$2(UY9x=^ zd&ibLJPb?=z%iJy(MuSPVR;w_ zC-8%m^M(cY!h}f+9>7NIr+@y zArpBVm*d9+3O#gK z8;fg#7LJU3Fn>}}ff7bySe*z*B8sL8kq@p$M`fkX+Eb^Dsda*HC#<|KBGd?t#yBW9 zD|8#5G1OvOFj8k7{NSp-wy+ZvHk_eulGpc+%X{K~Am*YU4O>&=4M&Ncq9+coZ?*9> zLTpqwg&r?98pL63Jgkn@;Sp-mkd%uvX^%O8!{pVXzJa7QK<$a@pQxmj2Fp+^78wdp zd>umg1W4;)1@VK!6jTT>L2vql$3t{HSq1@{yZ(6#&;W~p>=6>IlSs69luQ>(!wY45 zm-wy~-u_(H#jIt%Vv(=75?bP`)10zJzHF6c9nzO3F2lNNWip)0eBmNrc)9wDd@VK` zTjHDknw1A4htVAqmu(2(C1nwgMV*nAdT3+NCr=$w{hD%ae z5I;aUlU{HNY2V6QehN~5r_QLH!&^V351O=X``N*Blq+%gaoQzqp*Y4$RNEKSg7|6R zz>ORUtKs7jMKT#Dcoz5>VKoqnt6FSag|dO}Yuv+=q@uDKy+;mJHXE;){Jiqo_NPE) z3tX;pT1%~y$S1YCcYN)9{}+vP+Cr@Y>iImM1u(Q8G| zl&uS*eB#UVnh_I49NAl4qp$vJk1d_a^i=5~Yf>IvXR&%)YE_8mkL_%v_l*OMrz&CVwWYFx0mDO0wRXB#9`NjG+q&W23l%XYFSjp1uq zwrN^IWcmL`;ZQ~`{jPGsg}Xs&l!AT__nlBeWAq;O09ONO0f5w*G~*ymsYXnjLoxDk zW9j!H2Jue-o&*tlD%20qJbW6z2zxWdKLe*cI z5%c>=f0Kt+UGy4eNUW*Xn@%OdA}?IYxt2GZw@|rfzAGViEb*Ol?>5z;k|ru1N_mr# z43j~BdIj!gX?#o*f3ugo;G%AHkn_zR@@~D0e%PG)T~&Eqw6_s&i|bO0mqc1NPwlHt z^LQD(%%4SC@uezP?i8A00+56CMnk5QAbaUF>!BO&fh&25-k z%gM;KUa)-WVlvz_BTKwzCEs&_d&UltVY_Jiu>&`e_SWX2*Qp^A(j%L8V7Ce}L6a$Y zuC=iEN9r%4Z1RtswD6Eo0Zy delta 3076 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 diff --git a/linedance-app/ui/__pycache__/tag_editor.cpython-312.pyc b/linedance-app/ui/__pycache__/tag_editor.cpython-312.pyc index f4e36db1a2bee5d96a22f726dab06c7871c2ecc4..0b10e7f705610c69fef7dc8b6107cdb6d8292edd 100644 GIT binary patch literal 21946 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 literal 26437 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 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 ────────────────────────────────────────────────────────────────── diff --git a/linedance-app/ui/playlist_panel.py b/linedance-app/ui/playlist_panel.py index 3e378989..ba1808d7 100644 --- a/linedance-app/ui/playlist_panel.py +++ b/linedance-app/ui/playlist_panel.py @@ -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}") diff --git a/linedance-app/ui/settings_dialog.py b/linedance-app/ui/settings_dialog.py index dcd7a3dc..c273519c 100644 --- a/linedance-app/ui/settings_dialog.py +++ b/linedance-app/ui/settings_dialog.py @@ -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 diff --git a/linedance-app/ui/tag_editor.py b/linedance-app/ui/tag_editor.py index 07e9d88d..1fd49040 100644 --- a/linedance-app/ui/tag_editor.py +++ b/linedance-app/ui/tag_editor.py @@ -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}")