Rettelsaer

This commit is contained in:
2026-04-13 07:23:37 +02:00
parent 45dcedaed4
commit bbd5690d72
22 changed files with 2026 additions and 538 deletions

View File

@@ -95,6 +95,9 @@ class MainWindow(QMainWindow):
self._connect_player_signals()
self._library_loaded.connect(self._apply_library)
self._db_ready.connect(self._on_db_ready)
self._login_success_signal.connect(self._on_login_success)
self._login_fail_signal.connect(self._on_login_fail)
self._status_signal.connect(self._set_status)
self._build_menu()
self._build_ui()
self._build_statusbar()
@@ -130,15 +133,15 @@ class MainWindow(QMainWindow):
# ── Filer ─────────────────────────────────────────────────────────────
file_menu = menubar.addMenu("Filer")
self._act_go_online = QAction("Gå online...", self)
self._act_go_online = QAction("Gå online", self)
self._act_go_online.setShortcut("Ctrl+L")
self._act_go_online.triggered.connect(self._go_online)
file_menu.addAction(self._act_go_online)
self._act_go_offline = QAction("Gå offline", self)
self._act_go_offline.triggered.connect(self._go_offline)
self._act_go_offline.setEnabled(False)
file_menu.addAction(self._act_go_offline)
self._act_sync = QAction("↕ Synkroniser nu", self)
self._act_sync.setShortcut("Ctrl+Shift+S")
self._act_sync.triggered.connect(self._manual_sync)
file_menu.addAction(self._act_sync)
file_menu.addSeparator()
@@ -287,28 +290,26 @@ class MainWindow(QMainWindow):
b.setCheckable(True)
return b
self._btn_prev = btn("|◀◀", size=52)
self._btn_play = btn("", "btn_play", size=72)
self._btn_stop = btn("", "btn_stop", size=52)
self._btn_next = btn("▶▶|", size=52)
self._btn_stop = btn("", "btn_stop", size=72)
self._btn_demo = btn(f"\n{self._demo_seconds} SEK", "btn_demo", size=64, checkable=True)
self._btn_prev.clicked.connect(self._prev_song)
self._btn_play.clicked.connect(self._toggle_play)
self._btn_stop.clicked.connect(self._stop)
self._btn_next.clicked.connect(self._next_song)
self._btn_demo.clicked.connect(self._toggle_demo)
layout.addWidget(self._btn_prev)
layout.addWidget(self._btn_play)
layout.addWidget(self._btn_stop)
layout.addWidget(self._btn_next)
layout.addSpacing(24)
sep1 = QFrame()
sep1.setFrameShape(QFrame.Shape.VLine)
sep1.setFixedWidth(1)
layout.addWidget(sep1)
layout.addSpacing(24)
layout.addWidget(self._btn_demo)
layout.addStretch()
@@ -319,7 +320,9 @@ class MainWindow(QMainWindow):
self._vol_slider = QSlider(Qt.Orientation.Horizontal)
self._vol_slider.setRange(0, 100)
self._vol_slider.setValue(self._settings.get("volume", 78))
self._vol_slider.setFixedWidth(100)
self._vol_slider.setFixedWidth(160)
self._vol_slider.setFixedHeight(36)
self._vol_slider.setObjectName("vol_slider")
self._vol_slider.valueChanged.connect(self._on_volume)
layout.addWidget(self._vol_slider)
@@ -336,7 +339,14 @@ class MainWindow(QMainWindow):
self._playlist_panel.song_selected.connect(self._load_song_by_idx)
self._playlist_panel.song_dropped.connect(self._on_song_dropped)
self._playlist_panel.event_started.connect(self._on_event_started)
self._playlist_panel.next_song_ready.connect(self._load_song)
self._playlist_panel.next_song_ready.connect(self._on_next_song_ready)
self._playlist_panel.playlist_changed.connect(self._on_playlist_changed)
# Debounce-timer til auto-sync — starter sync 5 sek efter sidst ændring
self._sync_debounce = QTimer(self)
self._sync_debounce.setSingleShot(True)
self._sync_debounce.setInterval(5000)
self._sync_debounce.timeout.connect(self._auto_sync)
self._library_panel = LibraryPanel()
self._library_panel.song_selected.connect(self._on_library_song_selected)
@@ -433,9 +443,12 @@ class MainWindow(QMainWindow):
QTimer.singleShot(200, self._reload_library)
# Signal til at opdatere biblioteket fra baggrundstråd
_library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list)
_db_ready = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
_file_changed_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
_library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list)
_db_ready = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
_file_changed_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
_login_success_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(str)
_login_fail_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(str)
_status_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(str, int)
def _reload_library(self):
"""Hent sange fra DB i baggrundstråd — thread-safe via signal."""
@@ -508,20 +521,36 @@ class MainWindow(QMainWindow):
try:
restored = self._playlist_panel.restore_active_playlist()
if restored:
# Hent den sang der er klar (current_idx sat af restore)
idx = self._playlist_panel._current_idx
song = self._playlist_panel.get_song(idx)
if self._playlist_panel.restore_event_state():
idx = self._playlist_panel._current_idx
# Event var i gang — genoptag
idx = self._playlist_panel._current_idx
song = self._playlist_panel.get_song(idx)
if song:
self._current_idx = idx
self._song_ended = False
self._load_song(song)
self._playlist_panel.set_current(idx)
self._set_status(
f"Event genoptaget ved: {song.get('title','')} — tryk ▶ for at fortsætte",
f"Event genoptaget ved: {song.get('title','')} — tryk ▶",
6000,
)
elif song:
# Normal opstart — load første sang klar
self._current_idx = idx
self._song_ended = False
self._load_song(song)
self._playlist_panel.set_current(idx)
self._set_status(
f"Klar: {song.get('title','')} — tryk ▶ for at starte",
4000,
)
except Exception:
pass
# Scan 30 sek efter opstart — fanger ændringer siden sidst
QTimer.singleShot(30000, self.start_background_scan)
def start_background_scan(self):
@@ -602,40 +631,104 @@ class MainWindow(QMainWindow):
def _auto_login(self):
"""Forsøg automatisk login med gemte oplysninger."""
username = self._settings.get("username", "")
password = self._settings.get("password", "")
username = self._settings.get("username", "")
password = self._settings.get("password", "")
server_url = self._settings.get("server_url", "http://localhost:8000").rstrip("/")
if not username or not password:
return
try:
import urllib.request, urllib.parse, json
data = urllib.parse.urlencode({"username": username, "password": password}).encode()
req = urllib.request.Request(
f"{API_URL}/auth/login", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
with urllib.request.urlopen(req, timeout=8) as resp:
body = json.loads(resp.read())
self._api_token = body.get("access_token")
self._api_url = API_URL
self._api_username = username
self._set_online_state(True)
self._set_status(f"Automatisk logget ind som {username}", 4000)
# Synkroniser dans-niveauer og navne
QTimer.singleShot(500, self._sync_dance_data)
except Exception:
self._set_status("Auto-login fejlede — kør Filer → Gå online manuelt", 5000)
def _run():
try:
import urllib.request, urllib.parse, json
data = urllib.parse.urlencode({"username": username, "password": password}).encode()
req = urllib.request.Request(
f"{server_url}/auth/login", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
with urllib.request.urlopen(req, timeout=8) as resp:
body = json.loads(resp.read())
self._api_token = body.get("access_token")
self._api_url = server_url
self._api_username = username
# Kald GUI-opdatering via signal — thread-safe
self._login_success_signal.emit(username)
except Exception as e:
self._login_fail_signal.emit(str(e))
import threading
threading.Thread(target=_run, daemon=True).start()
def _on_playlist_changed(self):
"""Danseliste ændret — start debounce-timer til auto-sync."""
if hasattr(self, "_sync_debounce"):
self._sync_debounce.start()
def _auto_sync(self):
"""Kør sync hvis vi er online — kaldes af debounce-timer."""
if not self._api_token:
return
if not hasattr(self, "_sync_manager") or not self._sync_manager:
return
self._sync_manager.push(
on_done=lambda r: self._status_signal.emit(
f"↑ Synkroniseret — {r.get('songs_synced', 0)} sange", 3000
),
on_error=lambda e: self._status_signal.emit(
f"⚠ Sync fejl: {e}", 8000
),
)
def _on_next_song_ready(self, song: dict):
"""Næste sang er klar — load den i afspilleren og markér orange."""
idx = self._playlist_panel._current_idx
self._current_idx = idx
self._song_ended = False
self._playlist_panel._song_ended = False
self._load_song(song)
self._playlist_panel.set_current(idx)
def _on_login_success(self, username: str):
"""Kaldes i GUI-tråden når login lykkes."""
self._set_online_state(True)
self._set_status(f"Logget ind som {username}", 4000)
def _on_login_fail(self, error: str):
"""Kaldes i GUI-tråden når login fejler."""
self._set_status(f"Login fejlede: {error}", 5000)
def _go_online(self):
dialog = LoginDialog(self)
if dialog.exec():
url, username, token = dialog.get_credentials()
self._api_url = url
self._api_token = token
self._api_username = username
self._set_online_state(True)
self._set_status(f"Online som {username}", 5000)
QTimer.singleShot(500, self._sync_dance_data)
"""Log ind/ud med gemte credentials."""
if self._api_token:
self._go_offline()
return
username = self._settings.get("username", "")
password = self._settings.get("password", "")
server_url = self._settings.get("server_url", "http://localhost:8000").rstrip("/")
if not username or not password:
self._set_status("Udfyld brugernavn og kodeord i Indstillinger → Online", 5000)
return
def _run():
try:
import urllib.request, urllib.parse, json
data = urllib.parse.urlencode({"username": username, "password": password}).encode()
req = urllib.request.Request(
f"{server_url}/auth/login", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
with urllib.request.urlopen(req, timeout=8) as resp:
body = json.loads(resp.read())
self._api_token = body.get("access_token")
self._api_url = server_url
self._api_username = username
self._login_success_signal.emit(username)
except Exception as e:
self._login_fail_signal.emit(str(e))
import threading
threading.Thread(target=_run, daemon=True).start()
def _sync_dance_data(self):
"""Synkroniser dans-niveauer og navne fra API."""
@@ -669,15 +762,56 @@ class MainWindow(QMainWindow):
self._set_status("Offline — arbejder lokalt", 3000)
def _set_online_state(self, online: bool):
self._act_go_online.setEnabled(not online)
self._act_go_offline.setEnabled(online)
if online:
name = self._api_username or "?"
self._conn_label.setText(f"● ONLINE ({name})")
self._conn_label.setStyleSheet("color: #2ecc71;")
self._act_go_online.setText("● Gå offline")
self._init_sync()
else:
self._conn_label.setText("● OFFLINE")
self._conn_label.setStyleSheet("color: #5a6070;")
self._act_go_online.setText("● Gå online")
self._sync_manager = None
def _init_sync(self):
"""Opret SyncManager og kør initial push+pull."""
try:
from local.local_db import DB_PATH
from local.sync_manager import SyncManager
server_url = self._settings.get("server_url", "http://localhost:8000")
self._sync_manager = SyncManager(
db_path=str(DB_PATH),
server_url=server_url,
token=self._api_token,
)
self._sync_manager.push_and_pull(
on_done=lambda r: self._status_signal.emit(
f"✓ Synkroniseret — {r['push']['songs_synced']} sange", 5000
),
on_error=lambda e: self._status_signal.emit(
f"⚠ Sync fejl: {e}", 5000
),
)
except Exception as e:
self._set_status(f"⚠ Sync fejl: {e}", 5000)
def _manual_sync(self):
if not self._api_token:
self._set_status("Log ind for at synkronisere", 3000)
return
if not hasattr(self, "_sync_manager") or not self._sync_manager:
self._init_sync()
return
self._set_status("Synkroniserer...", 2000)
self._sync_manager.push_and_pull(
on_done=lambda r: self._status_signal.emit(
f"✓ Synkroniseret — {r['push']['songs_synced']} sange", 4000
),
on_error=lambda e: self._status_signal.emit(
f"⚠ Sync fejl: {e}", 5000
),
)
def _new_playlist(self):
self._stop()
@@ -851,6 +985,12 @@ class MainWindow(QMainWindow):
song = self._playlist_panel.get_song(idx)
if not song:
return
# Nulstil gammel markering
old_idx = self._playlist_panel._current_idx
if old_idx is not None and old_idx != idx:
if 0 <= old_idx < len(self._playlist_panel._statuses):
if self._playlist_panel._statuses[old_idx] == "playing":
self._playlist_panel._statuses[old_idx] = "pending"
self._current_idx = idx
self._load_song(song)
self._playlist_panel.set_current(idx)
@@ -944,13 +1084,16 @@ class MainWindow(QMainWindow):
self._btn_play.setText("")
self._vu.reset()
# Synkroniser current_idx til playlist_panel
self._playlist_panel._current_idx = self._current_idx
# Markér den afspillede sang
self._playlist_panel.mark_played(self._current_idx)
# Synkroniser event-status til den gemte navngivne liste
self._sync_event_status_to_playlist()
# Find første ikke-afspillede og ikke-skippede sang fra TOPPEN
# Find ste uafspillede
ni = self._playlist_panel.next_playable_idx()
next_song = self._playlist_panel.get_song(ni) if ni is not None else None
if next_song:
@@ -959,7 +1102,6 @@ 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