Næste version

This commit is contained in:
2026-04-12 10:25:41 +02:00
parent b678787236
commit 57f3c913b4
18 changed files with 2690 additions and 458 deletions

View File

@@ -5,7 +5,7 @@ playlist_panel.py — Danseliste med Ny/Gem/Hent knapper, autogem og event-overb
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView,
QMessageBox, QInputDialog,
QMessageBox,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QByteArray
from PyQt6.QtGui import QColor, QDragEnterEvent, QDropEvent
@@ -76,6 +76,13 @@ class PlaylistPanel(QWidget):
btn_save.clicked.connect(self._save_as)
toolbar.addWidget(btn_save)
self._btn_save_current = QPushButton("💾 Gem")
self._btn_save_current.setFixedHeight(26)
self._btn_save_current.setToolTip("Gem ændringer til den indlæste liste")
self._btn_save_current.clicked.connect(self._save_current)
self._btn_save_current.setEnabled(False)
toolbar.addWidget(self._btn_save_current)
btn_load = QPushButton("📂 Hent...")
btn_load.setFixedHeight(26)
btn_load.setToolTip("Hent en tidligere gemt danseliste")
@@ -106,8 +113,24 @@ class PlaylistPanel(QWidget):
self._lbl_progress.setObjectName("result_count")
ctrl.addWidget(self._lbl_progress)
btn_info = QPushButton("")
btn_info.setFixedSize(24, 28)
btn_info.setToolTip("Danseliste-info: samlet tid, pause-tid m.m.")
btn_info.clicked.connect(self._show_playlist_info)
ctrl.addWidget(btn_info)
layout.addLayout(ctrl)
# Pause-tid per dans (skjult men kan vises via info)
try:
from ui.settings_dialog import load_settings
s = load_settings()
self._pause_seconds = s.get("between_seconds", 60)
self._workshop_seconds = s.get("workshop_minutes", 10) * 60
except Exception:
self._pause_seconds = 60
self._workshop_seconds = 600
# ── Liste ─────────────────────────────────────────────────────────────
self._list = QListWidget()
self._list.setObjectName("playlist_list")
@@ -201,6 +224,11 @@ class PlaylistPanel(QWidget):
def _on_rows_moved(self, parent, start, end, dest, dest_row):
"""Opdater _songs og _statuses når en sang flyttes via drag."""
# Husk hvilken sang der er aktiv
current_song_id = None
if 0 <= self._current_idx < len(self._songs):
current_song_id = self._songs[self._current_idx].get("id")
new_songs = []
new_statuses = []
for i in range(self._list.count()):
@@ -210,17 +238,20 @@ class PlaylistPanel(QWidget):
new_statuses.append(self._statuses[old_idx])
self._songs = new_songs
self._statuses = new_statuses
self._current_idx = -1
# Gendan current_idx til den sang der stadig spiller
if current_song_id:
for i, s in enumerate(self._songs):
if s.get("id") == current_song_id:
self._current_idx = i
break
else:
self._current_idx = -1
self._song_ended = False
self._refresh()
self._trigger_autosave()
# Find første afspilbare sang og udsend signal så afspilleren opdateres
ni = self.next_playable_idx()
if ni is not None:
self._current_idx = ni
self._refresh()
self.next_song_ready.emit(self._songs[ni])
# Emit IKKE next_song_ready — afspilning fortsætter uforstyrret
# ── Event-state ───────────────────────────────────────────────────────────
@@ -292,42 +323,95 @@ class PlaylistPanel(QWidget):
self._lbl_autosave.setText(f"⚠ gemfejl")
pass
def _save_named_playlist_id(self, pl_id: int | None):
"""Gem hvilken navngiven liste der er aktiv — til brug ved næste opstart."""
from PyQt6.QtCore import QSettings
s = QSettings("LineDance", "Player")
if pl_id:
s.setValue("session/named_playlist_id", pl_id)
else:
s.remove("session/named_playlist_id")
def restore_active_playlist(self):
"""Indlæs den sidst aktive liste ved opstart."""
"""Gendan senest aktive navngivne liste med event-fremgang ved opstart."""
try:
from local.local_db import get_db
with get_db() as conn:
pl = conn.execute(
"SELECT id FROM playlists WHERE name=?", (ACTIVE_PLAYLIST_NAME,)
).fetchone()
if not pl:
return False
songs_raw = conn.execute("""
SELECT s.*, ps.position 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 = []
for row in songs_raw:
dances = conn.execute(
"SELECT dance_name FROM song_dances WHERE song_id=? ORDER BY dance_order",
(row["id"],)
).fetchall()
songs.append({
"id": row["id"], "title": row["title"],
"artist": row["artist"], "album": row["album"],
"bpm": row["bpm"], "duration_sec": row["duration_sec"],
"local_path": row["local_path"], "file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": [d["dance_name"] for d in dances],
})
from PyQt6.QtCore import QSettings
s = QSettings("LineDance", "Player")
pl_id = s.value("session/named_playlist_id", None, type=int)
if not pl_id:
return False
import sqlite3
from local.local_db import DB_PATH
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
# Verificer at listen stadig eksisterer
pl = conn.execute(
"SELECT id, name FROM playlists WHERE id=?", (pl_id,)
).fetchone()
if not pl:
conn.close()
return False
# Hent sange med status, workshop og dans-override
songs_raw = conn.execute("""
SELECT s.*, ps.position, ps.status,
ps.is_workshop, ps.dance_override
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 d.name FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
dance_names = [d["name"] for d in dances]
override = row["dance_override"] or ""
active_dance = override if override else (dance_names[0] if dance_names else "")
songs.append({
"id": row["id"],
"title": row["title"],
"artist": row["artist"],
"album": row["album"],
"bpm": row["bpm"],
"duration_sec": row["duration_sec"],
"local_path": row["local_path"],
"file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": dance_names,
"active_dance": active_dance,
"is_workshop": bool(row["is_workshop"]),
})
statuses.append(row["status"] or "pending")
conn.close()
if songs:
self._songs = songs
self._statuses = ["pending"] * len(songs)
self._refresh()
self._songs = songs
self._statuses = statuses
self._named_playlist_id = pl_id
self._current_idx = -1
self._song_ended = False
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl['name']}'")
self._title_label.setText(f"DANSELISTE — {pl['name'].upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._refresh()
# Find næste uafspillede og sæt den klar
ni = self.next_playable_idx()
if ni is not None:
self._current_idx = ni
self._refresh()
self.next_song_ready.emit(self._songs[ni])
return True
except Exception as e:
except Exception:
pass
return False
@@ -346,6 +430,10 @@ class PlaylistPanel(QWidget):
self._statuses = []
self._current_idx = -1
self._song_ended = False
self._named_playlist_id = None
self._btn_save_current.setEnabled(False)
self._btn_save_current.setToolTip("Gem ændringer til den indlæste liste")
self._save_named_playlist_id(None)
self._title_label.setText("DANSELISTE — NY")
self._refresh()
self._trigger_autosave()
@@ -354,75 +442,88 @@ class PlaylistPanel(QWidget):
if not self._songs:
QMessageBox.information(self, "Gem", "Danselisten er tom.")
return
name, ok = QInputDialog.getText(
self, "Gem danseliste", "Navn på danselisten:",
from ui.playlist_browser import PlaylistBrowserDialog
current_name = self._title_label.text().replace("DANSELISTE — ", "").replace("DANSELISTE", "").strip()
dialog = PlaylistBrowserDialog(
mode="save",
current_songs=self._songs,
current_name=current_name,
parent=self.window()
)
if not ok or not name.strip():
return
name = name.strip()
try:
from local.local_db import create_playlist, add_song_to_playlist
pl_id = create_playlist(name)
for i, song in enumerate(self._songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"], position=i)
def on_saved(pl_id, name):
self._named_playlist_id = pl_id
self._title_label.setText(f"DANSELISTE — {name.upper()}")
self._lbl_autosave.setText(f"✓ gemt som \"{name}\"")
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{name}'")
self._save_named_playlist_id(pl_id)
dialog.playlist_selected.connect(on_saved)
dialog.exec()
def _save_current(self):
"""Gem ændringer tilbage til den aktuelt indlæste navngivne liste."""
if not self._named_playlist_id:
return
if not self._songs:
QMessageBox.information(self, "Gem", "Danselisten er tom.")
return
try:
from local.local_db import get_db
with get_db() as conn:
conn.execute(
"DELETE FROM playlist_songs WHERE playlist_id=?",
(self._named_playlist_id,)
)
for i, song in enumerate(self._songs, start=1):
if song.get("id"):
status = self._statuses[i-1] if i-1 < len(self._statuses) else "pending"
conn.execute(
"INSERT INTO playlist_songs "
"(playlist_id, song_id, position, status) VALUES (?,?,?,?)",
(self._named_playlist_id, song["id"], i, status)
)
self._lbl_autosave.setText("✓ gemt")
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
def _load_dialog(self):
"""Vis liste af gemte danselister og lad brugeren vælge."""
try:
from local.local_db import get_db
with get_db() as conn:
lists = conn.execute(
"SELECT id, name, created_at FROM playlists "
"WHERE name != ? ORDER BY created_at DESC",
(ACTIVE_PLAYLIST_NAME,)
).fetchall()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke hente lister: {e}")
return
if not lists:
QMessageBox.information(self, "Hent liste", "Ingen gemte danselister fundet.")
return
names = [f"{row['name']} ({row['created_at'][:10]})" for row in lists]
choice, ok = QInputDialog.getItem(
self, "Hent danseliste", "Vælg en liste:", names, editable=False
)
if not ok:
return
idx = names.index(choice)
pl_id = lists[idx]["id"]
pl_name = lists[idx]["name"]
from ui.playlist_browser import PlaylistBrowserDialog
dialog = PlaylistBrowserDialog(mode="load", parent=self.window())
dialog.playlist_selected.connect(self._load_playlist_by_id)
dialog.exec()
def _load_playlist_by_id(self, pl_id: int, pl_name: str):
try:
from local.local_db import get_db
with get_db() as conn:
songs_raw = conn.execute("""
SELECT s.*, ps.position, ps.status FROM playlist_songs ps
SELECT s.*, ps.position, ps.status,
ps.is_workshop, ps.dance_override
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",
(row["id"],)
).fetchall()
dances = conn.execute("""
SELECT d.name FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
dance_names = [d["name"] for d in dances]
# dance_override bestemmer hvilken dans der vises
override = row["dance_override"] or ""
active_dance = override if override else (dance_names[0] if dance_names else "")
songs.append({
"id": row["id"], "title": row["title"],
"artist": row["artist"], "album": row["album"],
"bpm": row["bpm"], "duration_sec": row["duration_sec"],
"local_path": row["local_path"], "file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": [d["dance_name"] for d in dances],
"dances": dance_names,
"active_dance": active_dance,
"is_workshop": bool(row["is_workshop"]),
})
statuses.append(row["status"] or "pending")
self._songs = songs
@@ -432,6 +533,9 @@ class PlaylistPanel(QWidget):
self._named_playlist_id = pl_id
self._title_label.setText(f"DANSELISTE — {pl_name.upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl_name}'")
self._save_named_playlist_id(pl_id)
self._refresh()
self._trigger_autosave()
except Exception as e:
@@ -439,6 +543,94 @@ class PlaylistPanel(QWidget):
# ── Start event ───────────────────────────────────────────────────────────
def _show_playlist_info(self):
"""Åbn/luk det flydende danseliste-info vindue."""
try:
if hasattr(self, "_info_window") and self._info_window \
and self._info_window.isVisible():
self._info_window.close()
self._info_window = None
return
except RuntimeError:
self._info_window = None
# Opdater defaults fra indstillinger ved åbning
try:
from ui.settings_dialog import load_settings
s = load_settings()
self._pause_seconds = s.get("between_seconds", 60)
self._workshop_seconds = s.get("workshop_minutes", 10) * 60
except Exception:
pass
from ui.playlist_info_dialog import PlaylistInfoWindow
from PyQt6.QtWidgets import QApplication
main = self.window()
self._info_window = PlaylistInfoWindow(self, parent=main)
QApplication.instance().aboutToQuit.connect(self._info_window.close)
if main:
geo = main.geometry()
self._info_window.move(geo.right() + 10, geo.top() + 100)
self._info_window.show()
def _change_dance(self, idx: int, song: dict):
"""Lad brugeren vælge/skrive hvilken dans der vises for dette nummer."""
from ui.dance_picker_dialog import DancePickerDialog
current = song.get("active_dance", "")
if not current:
dances = song.get("dances", [])
current = dances[0] if dances else ""
dlg = DancePickerDialog(
current_dance=current,
song_title=song.get("title", ""),
parent=self.window()
)
if dlg.exec():
chosen = dlg.get_dance()
if chosen:
song["active_dance"] = chosen
self._refresh()
self._sync_dance_to_db(idx, song)
def _sync_dance_to_db(self, idx: int, song: dict):
"""Gem dance_override til playlist_songs."""
if not self._named_playlist_id:
return
try:
from local.local_db import get_db
with get_db() as conn:
conn.execute(
"UPDATE playlist_songs SET dance_override=? "
"WHERE playlist_id=? AND position=?",
(song.get("active_dance", ""), self._named_playlist_id, idx + 1)
)
except Exception:
pass
def _sync_ws_to_db(self, idx: int, song: dict):
"""Gem is_workshop til playlist_songs — både navngiven og aktiv liste."""
pl_ids = []
if self._named_playlist_id:
pl_ids.append(self._named_playlist_id)
if self._active_playlist_id:
pl_ids.append(self._active_playlist_id)
if not pl_ids:
return
try:
from local.local_db import get_db
with get_db() as conn:
for pl_id in pl_ids:
conn.execute(
"UPDATE playlist_songs SET is_workshop=? "
"WHERE playlist_id=? AND position=?",
(1 if song.get("is_workshop") else 0, pl_id, idx + 1)
)
except Exception:
pass
def _on_pause_changed(self, seconds: int):
self._pause_seconds = seconds
def _start_event(self):
if not self._songs:
return
@@ -469,6 +661,7 @@ class PlaylistPanel(QWidget):
idx = item.data(Qt.ItemDataRole.UserRole)
if idx is None:
return
song = self._songs[idx] if 0 <= idx < len(self._songs) else None
menu = QMenu(self)
act_play = menu.addAction("▶ Afspil denne")
menu.addSeparator()
@@ -476,6 +669,14 @@ class PlaylistPanel(QWidget):
act_unplay = menu.addAction("↺ Sæt til ikke afspillet")
act_played = menu.addAction("✓ Sæt til afspillet")
menu.addSeparator()
# Dans-valg
act_dance = menu.addAction("💃 Vælg dans...")
# Workshop toggle
is_ws = song.get("is_workshop", False) if song else False
act_ws = menu.addAction("🎓 Fjern workshop" if is_ws else "🎓 Markér som workshop")
menu.addSeparator()
act_dance_info = menu.addAction(" Dans-info...")
menu.addSeparator()
act_remove = menu.addAction("✕ Fjern fra liste")
action = menu.exec(self._list.mapToGlobal(pos))
if action == act_play:
@@ -492,6 +693,18 @@ class PlaylistPanel(QWidget):
self._statuses[idx] = "played"
self.status_changed.emit(idx, "played")
self._refresh(); self._trigger_autosave(); self._trigger_event_state_save()
elif action == act_dance and song:
self._change_dance(idx, song)
elif action == act_ws and song:
song["is_workshop"] = not song.get("is_workshop", False)
self._sync_ws_to_db(idx, song)
self._refresh()
self.playlist_changed.emit()
elif action == act_dance_info:
if song:
from ui.dance_info_dialog import DanceInfoDialog
dlg = DanceInfoDialog(song, parent=self.window())
dlg.exec()
elif action == act_remove:
self._songs.pop(idx)
self._statuses.pop(idx)
@@ -507,17 +720,22 @@ class PlaylistPanel(QWidget):
self._lbl_progress.setText(f"{played} / {len(self._songs)} afspillet")
for i, song in enumerate(self._songs):
is_current = (i == self._current_idx and not self._song_ended)
is_next = (self._song_ended and i == self._current_idx + 1) or \
(self._current_idx == -1 and self._song_ended and i == 0)
status = "playing" if is_current else "next" if is_next else self._statuses[i]
status = "playing" if is_current else self._statuses[i]
icon = self.STATUS_ICON.get(status, " ")
dances = " / ".join(song.get("dances", [])) or "ingen dans tagget"
text = f"{i+1:>2}. {song.get('title','')}\n {song.get('artist','')} · {dances}"
item = QListWidgetItem(f"{icon} {text}")
# Vis active_dance (override eller første dans) eller alle danse
active = song.get("active_dance", "")
if not active:
dances = song.get("dances", [])
active = dances[0] if dances else "ingen dans tagget"
ws_tag = " 🎓" if song.get("is_workshop") else ""
text = (f"{i+1:>2}. {song.get('title','')}{ws_tag}\n"
f" {song.get('artist','')} · {active}")
item = QListWidgetItem(f"{icon} {text}")
item.setData(Qt.ItemDataRole.UserRole, i)
color = self.STATUS_COLOR.get(status, "#5a6070")
if status in ("playing", "next"):
item.setForeground(QColor(color))
if status == "playing":
item.setForeground(QColor(self.STATUS_COLOR["playing"]))
f = item.font(); f.setBold(True); item.setFont(f)
elif status == "played":
item.setForeground(QColor("#2ecc71"))