Næster version

This commit is contained in:
2026-04-14 14:05:11 +02:00
parent 9257f198eb
commit 66804681da
11 changed files with 647 additions and 364 deletions

View File

@@ -1,10 +1,10 @@
""" """
sharing.py — Del playlister med andre brugere. sharing.py — Forenklet deling af playlister.
Kun ejeren kan redigere. Delte brugere får read-only via sync.
""" """
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from typing import Optional
from app.core.database import get_db from app.core.database import get_db
from app.core.security import get_current_user from app.core.security import get_current_user
@@ -15,21 +15,9 @@ router = APIRouter(prefix="/sharing", tags=["sharing"])
class ShareRequest(BaseModel): class ShareRequest(BaseModel):
email: EmailStr email: EmailStr
permission: str = "view" # view | copy | edit
class ShareOut(BaseModel): # ── Del med bruger ────────────────────────────────────────────────────────────
id: str
project_id: str
invited_email: str
permission: str
accepted_at: Optional[str] = None
class Config:
from_attributes = True
# ── Del en playliste ──────────────────────────────────────────────────────────
@router.post("/playlists/{project_id}/share", status_code=201) @router.post("/playlists/{project_id}/share", status_code=201)
async def share_playlist( async def share_playlist(
@@ -39,35 +27,27 @@ async def share_playlist(
db: Session = Depends(get_db), db: Session = Depends(get_db),
me: User = Depends(get_current_user), me: User = Depends(get_current_user),
): ):
"""Del en playliste med en bruger — de får listen ved næste sync."""
project = db.query(Project).filter_by(id=project_id, owner_id=me.id).first() project = db.query(Project).filter_by(id=project_id, owner_id=me.id).first()
if not project: if not project:
raise HTTPException(404, "Playliste ikke fundet eller du er ikke ejer") raise HTTPException(404, "Playliste ikke fundet eller du er ikke ejer")
if data.permission not in ("view", "copy", "edit"):
raise HTTPException(400, "Ugyldig rettighed — brug view, copy eller edit")
# Find bruger via email
target = db.query(User).filter_by(email=data.email).first() target = db.query(User).filter_by(email=data.email).first()
# Tjek om deling allerede eksisterer
existing = db.query(PlaylistShare).filter_by( existing = db.query(PlaylistShare).filter_by(
project_id=project_id, project_id=project_id, invited_email=data.email
invited_email=data.email,
).first() ).first()
if existing: if existing:
existing.permission = data.permission return {"detail": "Allerede delt med denne bruger"}
db.commit()
return {"detail": "Rettigheder opdateret", "share_id": existing.id}
share = PlaylistShare( share = PlaylistShare(
project_id=project_id, project_id=project_id,
shared_with_id=target.id if target else None, shared_with_id=target.id if target else None,
invited_email=data.email, invited_email=data.email,
permission=data.permission, permission="view",
) )
db.add(share) db.add(share)
db.commit() db.commit()
db.refresh(share)
# Send invitation-mail # Send invitation-mail
try: try:
@@ -78,32 +58,13 @@ async def share_playlist(
email=data.email, email=data.email,
owner_name=me.username, owner_name=me.username,
playlist_name=project.name, playlist_name=project.name,
permission=data.permission, permission="view",
accept_url=f"{settings.BASE_URL}/sharing/accept/{share.id}", accept_url=f"{settings.BASE_URL}/sharing/playlists/{project_id}",
) )
except Exception: except Exception:
pass pass
return {"detail": "Invitation sendt", "share_id": share.id} return {"detail": f"Delt med {data.email}"}
@router.patch("/playlists/{project_id}/share/{share_id}")
def update_share(
project_id: str,
share_id: str,
data: ShareRequest,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
project = db.query(Project).filter_by(id=project_id, owner_id=me.id).first()
if not project:
raise HTTPException(404, "Playliste ikke fundet")
share = db.query(PlaylistShare).filter_by(id=share_id, project_id=project_id).first()
if not share:
raise HTTPException(404, "Deling ikke fundet")
share.permission = data.permission
db.commit()
return {"detail": "Rettigheder opdateret"}
@router.delete("/playlists/{project_id}/share/{share_id}", status_code=204) @router.delete("/playlists/{project_id}/share/{share_id}", status_code=204)
@@ -133,15 +94,7 @@ def list_shares(
if not project: if not project:
raise HTTPException(404, "Playliste ikke fundet") raise HTTPException(404, "Playliste ikke fundet")
shares = db.query(PlaylistShare).filter_by(project_id=project_id).all() shares = db.query(PlaylistShare).filter_by(project_id=project_id).all()
return [ return [{"id": s.id, "email": s.invited_email} for s in shares]
{
"id": s.id,
"email": s.invited_email,
"permission": s.permission,
"accepted": s.accepted_at is not None,
}
for s in shares
]
# ── Visibility ──────────────────────────────────────────────────────────────── # ── Visibility ────────────────────────────────────────────────────────────────
@@ -154,64 +107,16 @@ def set_visibility(
me: User = Depends(get_current_user), me: User = Depends(get_current_user),
): ):
if visibility not in ("private", "shared", "public"): if visibility not in ("private", "shared", "public"):
raise HTTPException(400, "Ugyldig synlighed — brug private, shared eller public") raise HTTPException(400, "Brug private, shared eller public")
project = db.query(Project).filter_by(id=project_id, owner_id=me.id).first() project = db.query(Project).filter_by(id=project_id, owner_id=me.id).first()
if not project: if not project:
raise HTTPException(404, "Playliste ikke fundet") raise HTTPException(404, "Playliste ikke fundet")
project.visibility = visibility project.visibility = visibility
db.commit() db.commit()
return {"detail": f"Synlighed sat til {visibility}"} return {"detail": f"Synlighed: {visibility}"}
# ── Hent delte lister ───────────────────────────────────────────────────────── # ── Hent playliste-indhold ────────────────────────────────────────────────────
@router.get("/playlists/shared-with-me")
def shared_with_me(
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Hent alle playlister der er delt med mig."""
# Via direkte deling
shares = db.query(PlaylistShare).filter_by(
shared_with_id=me.id
).all()
project_ids = {s.project_id for s in shares}
# Via email-invitation
email_shares = db.query(PlaylistShare).filter_by(
invited_email=me.email
).all()
project_ids.update(s.project_id for s in email_shares)
# Public playlister
public = db.query(Project).filter_by(visibility="public").all()
project_ids.update(p.id for p in public)
result = []
for pid in project_ids:
p = db.query(Project).filter_by(id=pid).first()
if not p or p.owner_id == me.id:
continue
# Find min rettighed
share = db.query(PlaylistShare).filter(
PlaylistShare.project_id == pid,
(PlaylistShare.shared_with_id == me.id) |
(PlaylistShare.invited_email == me.email)
).first()
permission = share.permission if share else "view"
owner = db.query(User).filter_by(id=p.owner_id).first()
result.append({
"project_id": p.id,
"name": p.name,
"owner": owner.username if owner else "?",
"visibility": p.visibility,
"permission": permission,
"song_count": len(p.project_songs),
})
return result
# ── Hent en delt playliste ────────────────────────────────────────────────────
@router.get("/playlists/{project_id}") @router.get("/playlists/{project_id}")
def get_shared_playlist( def get_shared_playlist(
@@ -219,12 +124,9 @@ def get_shared_playlist(
db: Session = Depends(get_db), db: Session = Depends(get_db),
me: User = Depends(get_current_user), me: User = Depends(get_current_user),
): ):
"""Hent indholdet af en delt playliste."""
p = db.query(Project).filter_by(id=project_id).first() p = db.query(Project).filter_by(id=project_id).first()
if not p: if not p:
raise HTTPException(404, "Playliste ikke fundet") raise HTTPException(404, "Playliste ikke fundet")
# Tjek adgang
if p.owner_id != me.id: if p.owner_id != me.id:
if p.visibility != "public": if p.visibility != "public":
share = db.query(PlaylistShare).filter( share = db.query(PlaylistShare).filter(
@@ -233,7 +135,7 @@ def get_shared_playlist(
(PlaylistShare.invited_email == me.email) (PlaylistShare.invited_email == me.email)
).first() ).first()
if not share: if not share:
raise HTTPException(403, "Du har ikke adgang til denne playliste") raise HTTPException(403, "Ingen adgang")
from app.models import Song from app.models import Song
songs = [] songs = []
@@ -244,151 +146,16 @@ def get_shared_playlist(
songs.append({ songs.append({
"title": song.title, "title": song.title,
"artist": song.artist, "artist": song.artist,
"album": song.album,
"bpm": song.bpm,
"duration_sec": song.duration_sec,
"position": ps.position, "position": ps.position,
"status": ps.status, "status": ps.status,
"is_workshop": ps.is_workshop, "is_workshop": ps.is_workshop,
"dance_override": ps.dance_override, "dance_override": ps.dance_override or "",
}) })
return { return {
"id": p.id, "id": p.id,
"name": p.name, "name": p.name,
"description": p.description, "description": p.description or "",
"visibility": p.visibility, "visibility": p.visibility,
"songs": sorted(songs, key=lambda x: x["position"]), "songs": sorted(songs, key=lambda x: x["position"]),
} }
# ── Opdater sange i en linket liste ──────────────────────────────────────────
class LinkedSongData(BaseModel):
title: str
artist: str = ""
position: int = 1
status: str = "pending"
is_workshop: bool = False
dance_override: str = ""
class LinkedSongsUpdate(BaseModel):
songs: list[LinkedSongData]
@router.put("/playlists/{project_id}/songs")
def update_linked_songs(
project_id: str,
data: LinkedSongsUpdate,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Opdater sange i en linket playliste — kræver edit-rettighed."""
from app.models import Song, ProjectSong
p = db.query(Project).filter_by(id=project_id).first()
if not p:
raise HTTPException(404, "Playliste ikke fundet")
# Tjek edit-rettighed
if p.owner_id != me.id:
share = db.query(PlaylistShare).filter(
PlaylistShare.project_id == project_id,
(PlaylistShare.shared_with_id == me.id) |
(PlaylistShare.invited_email == me.email)
).first()
if not share or share.permission != "edit":
raise HTTPException(403, "Du har ikke redigerings-rettighed")
# Slet eksisterende sange og geninsert
db.query(ProjectSong).filter_by(project_id=project_id).delete()
for song_data in data.songs:
song = db.query(Song).filter_by(
title=song_data.title, artist=song_data.artist
).first()
if not song:
continue
ps = ProjectSong(
project_id=project_id,
song_id=song.id,
position=song_data.position,
status=song_data.status,
is_workshop=song_data.is_workshop,
dance_override=song_data.dance_override,
)
db.add(ps)
db.commit()
return {"detail": "Liste opdateret", "songs": len(data.songs)}
# ── Opdater sange på en delt playliste ───────────────────────────────────────
class LinkedSongData(BaseModel):
title: str
artist: str
position: int
status: str = "pending"
is_workshop: bool = False
dance_override: str = ""
class LinkedPlaylistUpdate(BaseModel):
songs: list[LinkedSongData]
@router.put("/playlists/{project_id}/songs")
def update_linked_playlist_songs(
project_id: str,
data: LinkedPlaylistUpdate,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Opdater sange på en delt playliste — kræver edit-rettighed."""
from app.models import Song
p = db.query(Project).filter_by(id=project_id).first()
if not p:
raise HTTPException(404, "Playliste ikke fundet")
# Tjek rettighed
if p.owner_id != me.id:
from app.models import PlaylistShare
share = db.query(PlaylistShare).filter(
PlaylistShare.project_id == project_id,
(PlaylistShare.shared_with_id == me.id) |
(PlaylistShare.invited_email == me.email)
).first()
if not share or share.permission != "edit":
raise HTTPException(403, "Du har ikke rettighed til at redigere denne liste")
# Slet eksisterende sange og indsæt nye
from app.models import ProjectSong
db.query(ProjectSong).filter_by(project_id=project_id).delete()
for song_data in data.songs:
# Match sang globalt på titel+artist
song = db.query(Song).filter_by(
title=song_data.title, artist=song_data.artist
).first()
if not song:
song = Song(
owner_id=me.id,
title=song_data.title,
artist=song_data.artist,
)
db.add(song)
db.flush()
ps = ProjectSong(
project_id=project_id,
song_id=song.id,
position=song_data.position,
status=song_data.status,
is_workshop=song_data.is_workshop,
dance_override=song_data.dance_override,
)
db.add(ps)
db.commit()
return {"detail": "Playliste opdateret", "songs": len(data.songs)}

View File

@@ -268,28 +268,37 @@ def pull(
"dance_id": cd.dance_id, "dance_id": cd.dance_id,
}) })
# Delte playlister # Delte playlister (read-only — kun ejeren kan redigere)
shared_ids = [ shared_ids = set()
s.project_id for s in for s in db.query(PlaylistShare).filter(
db.query(PlaylistShare).filter_by(shared_with_id=me.id).all() (PlaylistShare.shared_with_id == me.id) |
] (PlaylistShare.invited_email == me.email)
).all():
shared_ids.add(s.project_id)
shared = [] shared = []
for p in db.query(Project).filter(Project.id.in_(shared_ids)).all(): for p in db.query(Project).filter(Project.id.in_(shared_ids)).all():
shared.append({ if p.owner_id == me.id:
"id": p.id, continue # Egne lister håndteres separat
"name": p.name, owner = db.query(User).filter_by(id=p.owner_id).first()
"owner_id": p.owner_id, songs_out = []
"visibility": p.visibility, for ps in p.project_songs:
"songs": [ song = db.query(Song).filter_by(id=ps.song_id).first()
{ if not song:
"song_id": ps.song_id, continue
songs_out.append({
"title": song.title,
"artist": song.artist,
"position": ps.position, "position": ps.position,
"status": ps.status, "status": ps.status,
"is_workshop": ps.is_workshop, "is_workshop": ps.is_workshop,
"dance_override": ps.dance_override, "dance_override": ps.dance_override or "",
} })
for ps in p.project_songs shared.append({
] "server_id": p.id,
"name": p.name,
"owner": owner.username if owner else "?",
"songs": sorted(songs_out, key=lambda x: x["position"]),
}) })
# Egne playlister # Egne playlister

View File

@@ -4,7 +4,6 @@ Paa Windows uden konsol skrives alt til ~/.linedance/app.log
""" """
import logging import logging
import sys
from pathlib import Path from pathlib import Path
LOG_PATH = Path.home() / ".linedance" / "app.log" LOG_PATH = Path.home() / ".linedance" / "app.log"
@@ -13,12 +12,6 @@ LOG_PATH = Path.home() / ".linedance" / "app.log"
def setup_logging(): def setup_logging():
LOG_PATH.parent.mkdir(parents=True, exist_ok=True) LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
handlers = [logging.FileHandler(LOG_PATH, encoding="utf-8")] handlers = [logging.FileHandler(LOG_PATH, encoding="utf-8")]
if sys.stdout and hasattr(sys.stdout, 'write'):
try:
sys.stdout.write("")
handlers.append(logging.StreamHandler(sys.stdout))
except Exception:
pass
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,

View File

@@ -312,5 +312,59 @@ class SyncManager:
song_data.get("dance_override","") or "")) song_data.get("dance_override","") or ""))
position += 1 position += 1
# Importer delte playlister (read-only — is_linked=1, server_permission='view')
for pl in data.get("shared", []):
server_id = pl.get("server_id")
name = pl.get("name", "")
owner = pl.get("owner", "?")
if not server_id or not name:
continue
existing = conn.execute(
"SELECT id FROM playlists WHERE api_project_id=?", (server_id,)
).fetchone()
if existing:
# Opdater sange fra server (ejer kan have ændret listen)
pl_id = existing["id"]
conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
else:
cur = conn.execute(
"INSERT INTO playlists (name, description, api_project_id, is_linked, server_permission) "
"VALUES (?,?,?,1,'view')",
(f"{name} ({owner})", "", server_id)
)
pl_id = cur.lastrowid
position = 1
for song_data in pl.get("songs", []):
title = song_data.get("title", "")
artist = song_data.get("artist", "")
if not title:
continue
local = conn.execute(
"SELECT id FROM songs WHERE title=? AND artist=? LIMIT 1",
(title, artist)
).fetchone()
if not local:
import uuid
new_id = str(uuid.uuid4())
conn.execute(
"INSERT OR IGNORE INTO songs (id, title, artist, file_missing) VALUES (?,?,?,1)",
(new_id, title, artist)
)
local_id = new_id
else:
local_id = local["id"]
conn.execute("""
INSERT OR IGNORE INTO playlist_songs
(playlist_id, song_id, position, status, is_workshop, dance_override)
VALUES (?,?,?,?,?,?)
""", (pl_id, local_id, position,
song_data.get("status","pending"),
1 if song_data.get("is_workshop") else 0,
song_data.get("dance_override","") or ""))
position += 1
conn.commit() conn.commit()
conn.close() conn.close()

View File

@@ -216,9 +216,38 @@ class Player(QObject):
self.levels_changed.emit(max(0.0, l), max(0.0, r)) self.levels_changed.emit(max(0.0, l), max(0.0, r))
def set_audio_device(self, device_id: str):
"""Sæt lydoutput-enhed. device_id fra get_audio_devices()."""
if VLC_AVAILABLE and self._media_player:
self._media_player.audio_output_device_set(None, device_id)
@staticmethod
def get_audio_devices() -> list[dict]:
"""Returner liste af tilgængelige lydenheder."""
if not VLC_AVAILABLE:
return []
try:
instance = vlc.Instance("--no-video", "--quiet")
mp = instance.media_player_new()
devices = []
d = mp.audio_output_device_enum()
if d:
node = d
while node:
devices.append({
"id": node.contents.device.decode("utf-8", errors="replace"),
"name": node.contents.description.decode("utf-8", errors="replace"),
})
node = node.contents.next
vlc.libvlc_audio_output_device_list_release(d)
mp.release()
instance.release()
return devices
except Exception:
return []
def _on_end_reached(self, event): def _on_end_reached(self, event):
"""Kaldes fra VLC's event-tråd — må IKKE røre Qt-objekter direkte.""" """Kaldes fra VLC's event-tråd — må IKKE røre Qt-objekter direkte."""
# QTimer.singleShot er thread-safe og sender alt til main thread
from PyQt6.QtCore import QTimer as _QTimer from PyQt6.QtCore import QTimer as _QTimer
_QTimer.singleShot(0, self._handle_end_in_main_thread) _QTimer.singleShot(0, self._handle_end_in_main_thread)
@@ -227,3 +256,77 @@ class Player(QObject):
self._poll_timer.stop() self._poll_timer.stop()
self.song_ended.emit() self.song_ended.emit()
self.state_changed.emit("stopped") self.state_changed.emit("stopped")
class PreviewPlayer(QObject):
"""Simpel preview-afspiller til bibliotek — ingen signals, bare play/stop."""
def __init__(self, parent=None):
super().__init__(parent)
self._volume = 78
self._device_id = ""
if VLC_AVAILABLE:
self._instance = vlc.Instance("--no-video", "--quiet")
self._mp = self._instance.media_player_new()
else:
self._mp = None
def play(self, path: str):
if not VLC_AVAILABLE or not self._mp:
return
from player.player import Player
vlc_path = Player._resolve_path(self, path)
media = self._instance.media_new(vlc_path)
self._mp.set_media(media)
self._mp.audio_set_volume(self._volume)
self._mp.play()
# Sæt lydenhed efter play — VLC nulstiller den ved ny media
if self._device_id:
self._mp.audio_output_device_set(None, self._device_id)
def pause(self):
if VLC_AVAILABLE and self._mp:
self._mp.pause()
def resume(self):
if VLC_AVAILABLE and self._mp:
self._mp.play()
def stop(self):
if VLC_AVAILABLE and self._mp:
self._mp.stop()
def seek(self, fraction: float):
if VLC_AVAILABLE and self._mp:
self._mp.set_position(fraction)
def is_playing(self) -> bool:
if VLC_AVAILABLE and self._mp:
return bool(self._mp.is_playing())
return False
def get_position(self) -> float:
if VLC_AVAILABLE and self._mp:
return max(0.0, self._mp.get_position())
return 0.0
def get_time(self) -> int:
if VLC_AVAILABLE and self._mp:
return max(0, self._mp.get_time() // 1000)
return 0
def get_duration(self) -> int:
if VLC_AVAILABLE and self._mp:
ms = self._mp.get_length()
return max(0, ms // 1000)
return 0
def set_volume(self, volume: int):
self._volume = volume
if VLC_AVAILABLE and self._mp:
self._mp.audio_set_volume(volume)
def set_audio_device(self, device_id: str):
self._device_id = device_id
if VLC_AVAILABLE and self._mp:
self._mp.audio_output_device_set(None, device_id)

View File

@@ -5,6 +5,7 @@ library_panel.py — Musikbibliotek med søgning og drag-and-drop til danseliste
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QListWidget, QListWidgetItem, QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QLineEdit, QLabel, QHBoxLayout, QPushButton, QLineEdit, QLabel, QHBoxLayout, QPushButton,
QFrame, QSlider, QCheckBox,
QAbstractItemView, QStyledItemDelegate, QAbstractItemView, QStyledItemDelegate,
) )
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray, QRect from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray, QRect
@@ -175,6 +176,73 @@ class LibraryPanel(QWidget):
self._list.setItemDelegate(DanseButtonDelegate(self._list)) self._list.setItemDelegate(DanseButtonDelegate(self._list))
layout.addWidget(self._list) layout.addWidget(self._list)
# Preview-afspiller bar
self._preview_bar = self._build_preview_bar()
self._preview_bar.setVisible(False)
layout.addWidget(self._preview_bar)
# Timer til preview progress opdatering
self._preview_timer = QTimer(self)
self._preview_timer.setInterval(200)
self._preview_timer.timeout.connect(self._update_preview_progress)
def _build_preview_bar(self) -> QWidget:
bar = QFrame()
bar.setObjectName("track_display")
bar.setFixedHeight(62)
row = QHBoxLayout(bar)
row.setContentsMargins(10, 8, 10, 8)
row.setSpacing(10)
# Play/pause knap — orange som hoved-afspiller
self._btn_preview_play = QPushButton("")
self._btn_preview_play.setFixedSize(36, 36)
self._btn_preview_play.setObjectName("btn_play_small")
self._btn_preview_play.setToolTip("Afspil / Pause")
self._btn_preview_play.clicked.connect(self._toggle_preview_playback)
row.addWidget(self._btn_preview_play)
# Stop knap
self._btn_preview_stop = QPushButton("")
self._btn_preview_stop.setFixedSize(30, 30)
self._btn_preview_stop.setObjectName("btn_stop_small")
self._btn_preview_stop.setToolTip("Stop preview")
self._btn_preview_stop.clicked.connect(self._stop_preview)
row.addWidget(self._btn_preview_stop)
# Titel + progress i midten
info = QVBoxLayout()
info.setSpacing(4)
info.setContentsMargins(0, 0, 0, 0)
self._lbl_preview_title = QLabel("")
self._lbl_preview_title.setObjectName("track_meta")
info.addWidget(self._lbl_preview_title)
self._preview_progress = QSlider(Qt.Orientation.Horizontal)
self._preview_progress.setRange(0, 1000)
self._preview_progress.sliderMoved.connect(self._seek_preview)
info.addWidget(self._preview_progress)
row.addLayout(info, stretch=1)
# Tid
self._lbl_preview_time = QLabel("0:00")
self._lbl_preview_time.setObjectName("track_meta")
self._lbl_preview_time.setFixedWidth(70)
self._lbl_preview_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
row.addWidget(self._lbl_preview_time)
# Volumen
self._preview_vol = QSlider(Qt.Orientation.Horizontal)
self._preview_vol.setRange(0, 100)
self._preview_vol.setValue(78)
self._preview_vol.setFixedWidth(70)
self._preview_vol.setToolTip("Volumen preview")
self._preview_vol.valueChanged.connect(self._on_preview_volume)
row.addWidget(self._preview_vol)
return bar
# ── Scanning ────────────────────────────────────────────────────────────── # ── Scanning ──────────────────────────────────────────────────────────────
def _on_scan_clicked(self): def _on_scan_clicked(self):
@@ -312,11 +380,6 @@ class LibraryPanel(QWidget):
# ── Handlinger ──────────────────────────────────────────────────────────── # ── Handlinger ────────────────────────────────────────────────────────────
def _on_double_click(self, item: QListWidgetItem):
song = item.data(Qt.ItemDataRole.UserRole)
if song:
self.song_selected.emit(song)
def _show_context_menu(self, pos): def _show_context_menu(self, pos):
from PyQt6.QtWidgets import QMenu from PyQt6.QtWidgets import QMenu
item = self._list.itemAt(pos) item = self._list.itemAt(pos)
@@ -328,6 +391,15 @@ class LibraryPanel(QWidget):
menu = QMenu(self) menu = QMenu(self)
act_add = menu.addAction("Tilføj til danseliste") act_add = menu.addAction("Tilføj til danseliste")
act_play = menu.addAction("Afspil") act_play = menu.addAction("Afspil")
# Preview kun hvis preview player er sat op
is_previewing = (
hasattr(self, "_preview_player") and
self._preview_player and
self._preview_player.is_playing()
)
act_preview = menu.addAction(
"⏹ Stop preview" if is_previewing else "▶ Preview (høretelefoner)"
)
menu.addSeparator() menu.addSeparator()
act_tags = menu.addAction("✎ Rediger dans-tags...") act_tags = menu.addAction("✎ Rediger dans-tags...")
act_info = menu.addAction(" Dans-info...") act_info = menu.addAction(" Dans-info...")
@@ -340,6 +412,8 @@ class LibraryPanel(QWidget):
self.add_to_playlist.emit(song) self.add_to_playlist.emit(song)
elif action == act_play: elif action == act_play:
self.song_selected.emit(song) self.song_selected.emit(song)
elif action == act_preview:
self._toggle_preview(song)
elif action == act_tags: elif action == act_tags:
self.edit_tags_requested.emit(song) self.edit_tags_requested.emit(song)
elif action == act_info: elif action == act_info:
@@ -351,6 +425,78 @@ class LibraryPanel(QWidget):
elif action == act_mail: elif action == act_mail:
self.send_mail_requested.emit(song) self.send_mail_requested.emit(song)
def set_preview_player(self, preview_player):
"""Sæt preview-afspilleren fra main_window."""
self._preview_player = preview_player
def _on_double_click(self, item: QListWidgetItem):
song = item.data(Qt.ItemDataRole.UserRole)
if song:
self._start_preview(song)
def _start_preview(self, song: dict):
if not hasattr(self, "_preview_player") or not self._preview_player:
self.song_selected.emit(song)
return
path = song.get("local_path", "")
if not path:
return
self._preview_song = song
self._preview_player.play(path)
title = song.get("title", "")
artist = song.get("artist", "")
self._lbl_preview_title.setText(f"{title} · {artist}")
self._btn_preview_play.setText("")
self._preview_bar.setVisible(True)
self._preview_timer.start()
def _toggle_preview_playback(self):
if not hasattr(self, "_preview_player") or not self._preview_player:
return
if self._preview_player.is_playing():
self._preview_player.pause()
self._btn_preview_play.setText("")
else:
self._preview_player.resume()
self._btn_preview_play.setText("")
def _stop_preview(self):
if hasattr(self, "_preview_player") and self._preview_player:
self._preview_player.stop()
self._preview_timer.stop()
self._preview_bar.setVisible(False)
self._btn_preview_play.setText("")
def _seek_preview(self, value: int):
if hasattr(self, "_preview_player") and self._preview_player:
self._preview_player.seek(value / 1000.0)
def _on_preview_volume(self, value: int):
if hasattr(self, "_preview_player") and self._preview_player:
self._preview_player.set_volume(value)
def _update_preview_progress(self):
if not hasattr(self, "_preview_player") or not self._preview_player:
return
if not self._preview_player.is_playing():
self._btn_preview_play.setText("")
return
pos = self._preview_player.get_position()
cur = self._preview_player.get_time()
dur = self._preview_player.get_duration()
self._preview_progress.setValue(int(pos * 1000))
def fmt(s): return f"{s//60}:{s%60:02d}"
self._lbl_preview_time.setText(f"{fmt(cur)} / {fmt(dur)}")
def _toggle_preview(self, song: dict):
"""Start/stop preview af en sang."""
if not hasattr(self, "_preview_player") or not self._preview_player:
return
if self._preview_player.is_playing():
self._stop_preview()
else:
self._start_preview(song)
def _analyze_bpm(self, song: dict): def _analyze_bpm(self, song: dict):
"""Analysér BPM i baggrundstråd og opdater biblioteket.""" """Analysér BPM i baggrundstråd og opdater biblioteket."""
path = song.get("local_path", "") path = song.get("local_path", "")

View File

@@ -76,6 +76,9 @@ class MainWindow(QMainWindow):
self._dark_theme = True self._dark_theme = True
self._player = Player(self) self._player = Player(self)
# Preview-afspiller til bibliotek (høretelefoner)
from player.player import PreviewPlayer
self._preview_player = PreviewPlayer(self)
self._current_idx = -1 self._current_idx = -1
self._song_ended = False self._song_ended = False
self._demo_active = False self._demo_active = False
@@ -349,6 +352,15 @@ class MainWindow(QMainWindow):
self._sync_debounce.timeout.connect(self._auto_sync) self._sync_debounce.timeout.connect(self._auto_sync)
self._library_panel = LibraryPanel() self._library_panel = LibraryPanel()
self._library_panel.set_preview_player(self._preview_player)
# Sæt audio devices fra indstillinger
main_device = self._settings.get("audio_device_main", "")
preview_device = self._settings.get("audio_device_preview", "")
if main_device:
self._player.set_audio_device(main_device)
if preview_device:
self._preview_player.set_audio_device(preview_device)
self._library_panel.song_selected.connect(self._on_library_song_selected) self._library_panel.song_selected.connect(self._on_library_song_selected)
self._library_panel.add_to_playlist.connect(self._add_song_to_playlist) self._library_panel.add_to_playlist.connect(self._add_song_to_playlist)
self._library_panel.scan_requested.connect(self.start_scan) self._library_panel.scan_requested.connect(self.start_scan)
@@ -628,6 +640,13 @@ class MainWindow(QMainWindow):
if dur > 0: if dur > 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._progress.set_demo_marker(min(self._demo_seconds / dur, 1.0), min((self._demo_seconds + self._demo_fade_seconds) / dur, 1.0))
self._set_status("Indstillinger gemt", 2000) self._set_status("Indstillinger gemt", 2000)
# Anvend lydenheder med det samme
main_device = self._settings.get("audio_device_main", "")
preview_device = self._settings.get("audio_device_preview", "")
if main_device:
self._player.set_audio_device(main_device)
if preview_device:
self._preview_player.set_audio_device(preview_device)
def _auto_login(self): def _auto_login(self):
"""Forsøg automatisk login med gemte oplysninger.""" """Forsøg automatisk login med gemte oplysninger."""
@@ -970,6 +989,10 @@ class MainWindow(QMainWindow):
dur = song.get("duration_sec", 0) dur = song.get("duration_sec", 0)
self._player.load(song.get("local_path", ""), dur) self._player.load(song.get("local_path", ""), dur)
# Stop preview hvis den kører
if hasattr(self, "_preview_player") and self._preview_player.is_playing():
self._library_panel._stop_preview()
self._lbl_title.setText(song.get("title", "")) self._lbl_title.setText(song.get("title", ""))
bpm = song.get("bpm", 0) bpm = song.get("bpm", 0)
fmt_dur = f"{dur//60}:{dur%60:02d}" fmt_dur = f"{dur//60}:{dur%60:02d}"
@@ -1006,6 +1029,7 @@ class MainWindow(QMainWindow):
self._btn_demo.setChecked(False) self._btn_demo.setChecked(False)
self._btn_play.setText("") self._btn_play.setText("")
return return
self._waiting_for_auto = False # annuller evt. auto-timer
if self._player.is_playing(): if self._player.is_playing():
self._player.pause() self._player.pause()
else: else:
@@ -1014,6 +1038,13 @@ class MainWindow(QMainWindow):
self._btn_play.setText("") self._btn_play.setText("")
def _stop(self): def _stop(self):
# Annuller evt. igangværende countdown
if getattr(self, "_waiting_for_auto", False):
self._waiting_for_auto = False
if hasattr(self, "_countdown_timer"):
self._countdown_timer.stop()
self._set_status("Auto-afspilning annulleret", 3000)
return
self._player.stop() self._player.stop()
self._song_ended = False self._song_ended = False
self._demo_active = False self._demo_active = False
@@ -1104,7 +1135,27 @@ class MainWindow(QMainWindow):
self._current_idx = ni self._current_idx = ni
self._playlist_panel.set_next_ready(ni) self._playlist_panel.set_next_ready(ni)
self._load_song(next_song) self._load_song(next_song)
mode = self._settings.get("after_song_mode", "manual")
delay = self._settings.get("after_song_delay", 2)
if mode == "manual":
self._waiting_for_auto = False
self._set_status(f"Klar: {next_song.get('title','')} — tryk ▶ for at starte") self._set_status(f"Klar: {next_song.get('title','')} — tryk ▶ for at starte")
elif mode in ("auto_demo", "auto_play"):
self._waiting_for_auto = True
self._countdown_secs = delay
self._countdown_mode = mode
self._countdown_title = next_song.get("title", "")
label = "Auto-demo" if mode == "auto_demo" else "Auto-play"
self._set_status(f"{label} om {delay}s — {self._countdown_title}")
if not hasattr(self, "_countdown_timer"):
self._countdown_timer = QTimer(self)
self._countdown_timer.setInterval(1000)
self._countdown_timer.timeout.connect(self._countdown_tick)
self._countdown_timer.start()
else: else:
self._current_idx = -1 self._current_idx = -1
self._playlist_panel._current_idx = -1 self._playlist_panel._current_idx = -1
@@ -1116,6 +1167,44 @@ class MainWindow(QMainWindow):
self._lbl_dances.setText("") self._lbl_dances.setText("")
self._set_status("Danselisten er afsluttet") self._set_status("Danselisten er afsluttet")
def _countdown_tick(self):
"""Tæller ned og starter auto-afspilning når den når 0."""
if not getattr(self, "_waiting_for_auto", False):
self._countdown_timer.stop()
return
self._countdown_secs -= 1
label = "Auto-demo" if self._countdown_mode == "auto_demo" else "Auto-play"
if self._countdown_secs > 0:
self._set_status(f"{label} om {self._countdown_secs}s — {self._countdown_title}")
else:
self._countdown_timer.stop()
if self._countdown_mode == "auto_demo":
self._auto_demo_next()
else:
self._auto_play_next()
def _auto_demo_next(self):
"""Afspil demo af den næste klargjorte sang automatisk."""
if not getattr(self, "_waiting_for_auto", False):
return # Brugeren har allerede trykket ▶ manuelt
self._waiting_for_auto = False
self._playlist_panel.set_current(self._current_idx)
self._player.play_demo(self._demo_seconds, self._demo_fade_seconds)
self._demo_active = True
self._btn_demo.setChecked(True)
self._btn_play.setText("")
self._song_ended = False
def _auto_play_next(self):
"""Start næste sang automatisk."""
if not getattr(self, "_waiting_for_auto", False):
return # Brugeren har allerede trykket ▶ manuelt
self._waiting_for_auto = False
self._playlist_panel.set_current(self._current_idx)
self._player.play()
self._btn_play.setText("")
self._song_ended = False
def _sync_event_status_to_playlist(self): def _sync_event_status_to_playlist(self):
"""Gem event-fremgang (afspillet/sprunget over) til den navngivne liste.""" """Gem event-fremgang (afspillet/sprunget over) til den navngivne liste."""
try: try:

View File

@@ -196,6 +196,11 @@ class PlaylistPanel(QWidget):
def set_next_ready(self, idx: int): def set_next_ready(self, idx: int):
"""Sæt næste sang klar — uden at overskrive skipped/played statusser.""" """Sæt næste sang klar — uden at overskrive skipped/played statusser."""
# Nulstil forrige current hvis den stadig er playing
old = self._current_idx
if old != idx and 0 <= old < len(self._statuses):
if self._statuses[old] == "playing":
self._statuses[old] = "pending"
self._current_idx = idx self._current_idx = idx
self._song_ended = False self._song_ended = False
# Ændr KUN status hvis den er pending — rør ikke skipped/played # Ændr KUN status hvis den er pending — rør ikke skipped/played
@@ -427,7 +432,11 @@ class PlaylistPanel(QWidget):
if songs: if songs:
self._songs = songs self._songs = songs
self._statuses = statuses # Rens "playing" — må kun være én ad gangen
self._statuses = [
"pending" if s == "playing" else s
for s in statuses
]
self._named_playlist_id = pl_id self._named_playlist_id = pl_id
self._current_idx = -1 self._current_idx = -1
self._song_ended = False self._song_ended = False
@@ -440,7 +449,6 @@ class PlaylistPanel(QWidget):
ni = self.next_playable_idx() ni = self.next_playable_idx()
if ni is not None: if ni is not None:
self._current_idx = ni self._current_idx = ni
self._statuses[ni] = "playing"
self._refresh() self._refresh()
return True return True

View File

@@ -24,6 +24,10 @@ SETTINGS_KEY_SERVER_URL = "online/server_url"
SETTINGS_KEY_LANGUAGE = "appearance/language" SETTINGS_KEY_LANGUAGE = "appearance/language"
SETTINGS_KEY_BETWEEN_SEC = "playback/between_seconds" SETTINGS_KEY_BETWEEN_SEC = "playback/between_seconds"
SETTINGS_KEY_WORKSHOP_MIN = "playback/workshop_minutes" SETTINGS_KEY_WORKSHOP_MIN = "playback/workshop_minutes"
SETTINGS_KEY_MAIN_DEVICE = "playback/audio_device_main"
SETTINGS_KEY_PREV_DEVICE = "playback/audio_device_preview"
SETTINGS_KEY_AFTER_SONG = "playback/after_song_mode"
SETTINGS_KEY_AFTER_DELAY = "playback/after_song_delay"
def load_settings() -> dict: def load_settings() -> dict:
@@ -42,6 +46,10 @@ def load_settings() -> dict:
"language": s.value(SETTINGS_KEY_LANGUAGE, "da"), "language": s.value(SETTINGS_KEY_LANGUAGE, "da"),
"between_seconds": s.value(SETTINGS_KEY_BETWEEN_SEC, 60, type=int), "between_seconds": s.value(SETTINGS_KEY_BETWEEN_SEC, 60, type=int),
"workshop_minutes": s.value(SETTINGS_KEY_WORKSHOP_MIN, 10, type=int), "workshop_minutes": s.value(SETTINGS_KEY_WORKSHOP_MIN, 10, type=int),
"audio_device_main": s.value(SETTINGS_KEY_MAIN_DEVICE, ""),
"audio_device_preview":s.value(SETTINGS_KEY_PREV_DEVICE, ""),
"after_song_mode": s.value(SETTINGS_KEY_AFTER_SONG, "manual"),
"after_song_delay": s.value(SETTINGS_KEY_AFTER_DELAY, 2, type=int),
} }
@@ -60,6 +68,10 @@ def save_settings(values: dict):
s.setValue(SETTINGS_KEY_LANGUAGE, values.get("language", "da")) s.setValue(SETTINGS_KEY_LANGUAGE, values.get("language", "da"))
s.setValue(SETTINGS_KEY_BETWEEN_SEC, values.get("between_seconds", 60)) s.setValue(SETTINGS_KEY_BETWEEN_SEC, values.get("between_seconds", 60))
s.setValue(SETTINGS_KEY_WORKSHOP_MIN,values.get("workshop_minutes", 10)) s.setValue(SETTINGS_KEY_WORKSHOP_MIN,values.get("workshop_minutes", 10))
s.setValue(SETTINGS_KEY_MAIN_DEVICE, values.get("audio_device_main", ""))
s.setValue(SETTINGS_KEY_PREV_DEVICE, values.get("audio_device_preview", ""))
s.setValue(SETTINGS_KEY_AFTER_SONG, values.get("after_song_mode", "manual"))
s.setValue(SETTINGS_KEY_AFTER_DELAY, values.get("after_song_delay", 2))
class SettingsDialog(QDialog): class SettingsDialog(QDialog):
@@ -191,6 +203,63 @@ class SettingsDialog(QDialog):
grp2_layout.addRow("Tid per workshop:", self._spin_workshop) grp2_layout.addRow("Tid per workshop:", self._spin_workshop)
layout.addWidget(grp2) layout.addWidget(grp2)
# Reaktion når sang slutter
from PyQt6.QtWidgets import QRadioButton, QButtonGroup
grp3 = QGroupBox("Når en sang slutter")
grp3_layout = QVBoxLayout(grp3)
grp3_layout.setSpacing(8)
self._radio_manual = QRadioButton("Manuel — marker næste klar, vent på ▶")
self._radio_auto_demo = QRadioButton("Auto-demo — afspil demo af næste sang automatisk")
self._radio_auto_play = QRadioButton("Auto-play — start næste sang automatisk")
self._after_song_group = QButtonGroup(self)
self._after_song_group.addButton(self._radio_manual, 0)
self._after_song_group.addButton(self._radio_auto_demo, 1)
self._after_song_group.addButton(self._radio_auto_play, 2)
grp3_layout.addWidget(self._radio_manual)
grp3_layout.addWidget(self._radio_auto_demo)
grp3_layout.addWidget(self._radio_auto_play)
delay_row = QHBoxLayout()
delay_row.addWidget(QLabel(" Pause før næste starter:"))
self._spin_after_delay = QSpinBox()
self._spin_after_delay.setRange(0, 30)
self._spin_after_delay.setSuffix(" sekunder")
self._spin_after_delay.setFixedWidth(160)
self._spin_after_delay.setToolTip(
"Bruges til auto-demo og auto-play.\n"
"Antal sekunder der ventes inden næste sang starter."
)
delay_row.addWidget(self._spin_after_delay)
delay_row.addStretch()
grp3_layout.addLayout(delay_row)
layout.addWidget(grp3)
grp3 = QGroupBox("Lydenheder")
grp3_layout = QFormLayout(grp3)
from player.player import Player as _Player
devices = _Player.get_audio_devices()
device_items = [("Standard", "")] + [(d["name"], d["id"]) for d in devices]
self._combo_main_device = QComboBox()
self._combo_preview_device = QComboBox()
for name, did in device_items:
self._combo_main_device.addItem(name, did)
self._combo_preview_device.addItem(name, did)
grp3_layout.addRow("Hoved-afspiller (sal):", self._combo_main_device)
grp3_layout.addRow("Preview (høretelefoner):", self._combo_preview_device)
note3 = QLabel("Preview-afspilleren bruges til at lytte til sange i biblioteket\nudenom at afbryde den sang der spiller i salen.")
note3.setObjectName("result_count")
note3.setWordWrap(True)
grp3_layout.addRow(note3)
layout.addWidget(grp3)
layout.addStretch() layout.addStretch()
return tab return tab
@@ -359,6 +428,28 @@ class SettingsDialog(QDialog):
self._user_input.setEnabled(auto) self._user_input.setEnabled(auto)
self._pass_input.setEnabled(auto) self._pass_input.setEnabled(auto)
# Lydenheder
main_dev = v.get("audio_device_main", "")
preview_dev = v.get("audio_device_preview", "")
for i in range(self._combo_main_device.count()):
if self._combo_main_device.itemData(i) == main_dev:
self._combo_main_device.setCurrentIndex(i)
break
for i in range(self._combo_preview_device.count()):
if self._combo_preview_device.itemData(i) == preview_dev:
self._combo_preview_device.setCurrentIndex(i)
break
# Reaktion når sang slutter
mode = v.get("after_song_mode", "manual")
if mode == "auto_demo":
self._radio_auto_demo.setChecked(True)
elif mode == "auto_play":
self._radio_auto_play.setChecked(True)
else:
self._radio_manual.setChecked(True)
self._spin_after_delay.setValue(v.get("after_song_delay", 2))
# ── Gem ─────────────────────────────────────────────────────────────────── # ── Gem ───────────────────────────────────────────────────────────────────
def _save_and_close(self): def _save_and_close(self):
@@ -375,6 +466,14 @@ class SettingsDialog(QDialog):
"password": self._pass_input.text(), "password": self._pass_input.text(),
"server_url": self._server_url.text().strip() or "http://localhost:8000", "server_url": self._server_url.text().strip() or "http://localhost:8000",
"language": self._lang_combo.currentData(), "language": self._lang_combo.currentData(),
"audio_device_main": self._combo_main_device.currentData() or "",
"audio_device_preview":self._combo_preview_device.currentData() or "",
"after_song_mode": (
"auto_demo" if self._radio_auto_demo.isChecked() else
"auto_play" if self._radio_auto_play.isChecked() else
"manual"
),
"after_song_delay": self._spin_after_delay.value(),
} }
save_settings(values) save_settings(values)
self._values = values self._values = values

View File

@@ -1,5 +1,6 @@
""" """
share_dialog.py — Del en playliste med andre brugere eller sæt den public. share_dialog.py — Forenklet del-dialog.
Kun ejer kan dele. Delte brugere får listen ved næste sync.
""" """
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
@@ -19,7 +20,7 @@ class ShareDialog(QDialog):
self._token = token self._token = token
self.setWindowTitle(f"Del — {playlist_name}") self.setWindowTitle(f"Del — {playlist_name}")
self.setMinimumWidth(480) self.setMinimumWidth(440)
self._build_ui() self._build_ui()
self._load_shares() self._load_shares()
self._load_visibility() self._load_visibility()
@@ -42,9 +43,9 @@ class ShareDialog(QDialog):
vis_layout.setContentsMargins(10, 8, 10, 8) vis_layout.setContentsMargins(10, 8, 10, 8)
vis_layout.addWidget(QLabel("Synlighed:")) vis_layout.addWidget(QLabel("Synlighed:"))
self._vis_combo = QComboBox() self._vis_combo = QComboBox()
self._vis_combo.addItem("🔒 Privat (kun mig)", "private") self._vis_combo.addItem("Privat kun dig", "private")
self._vis_combo.addItem("👥 Delt (inviterede)", "shared") self._vis_combo.addItem("Delt — kun inviterede", "shared")
self._vis_combo.addItem("🌐 Public (alle kan se)", "public") self._vis_combo.addItem("Public — hjemmesiden", "public")
vis_layout.addWidget(self._vis_combo, stretch=1) vis_layout.addWidget(self._vis_combo, stretch=1)
btn_vis = QPushButton("Gem") btn_vis = QPushButton("Gem")
btn_vis.setFixedHeight(28) btn_vis.setFixedHeight(28)
@@ -52,38 +53,34 @@ class ShareDialog(QDialog):
vis_layout.addWidget(btn_vis) vis_layout.addWidget(btn_vis)
layout.addWidget(vis_frame) layout.addWidget(vis_frame)
# Invitér bruger # Del med bruger
inv_frame = QFrame() inv_frame = QFrame()
inv_frame.setObjectName("track_display") inv_frame.setObjectName("track_display")
inv_layout = QVBoxLayout(inv_frame) inv_layout = QVBoxLayout(inv_frame)
inv_layout.setContentsMargins(10, 8, 10, 8) inv_layout.setContentsMargins(10, 8, 10, 8)
inv_layout.setSpacing(6) inv_layout.setSpacing(6)
inv_layout.addWidget(QLabel("Invitér via e-mail:")) inv_layout.addWidget(QLabel("Del med (e-mail):"))
row = QHBoxLayout() row = QHBoxLayout()
self._email_input = QLineEdit() self._email_input = QLineEdit()
self._email_input.setPlaceholderText("bruger@eksempel.dk") self._email_input.setPlaceholderText("bruger@eksempel.dk")
row.addWidget(self._email_input) row.addWidget(self._email_input)
self._perm_combo = QComboBox() btn_inv = QPushButton("Del")
self._perm_combo.addItem("Se", "view")
self._perm_combo.addItem("Kopiere", "copy")
self._perm_combo.addItem("Redigere","edit")
self._perm_combo.setFixedWidth(90)
row.addWidget(self._perm_combo)
btn_inv = QPushButton("Invitér")
btn_inv.setFixedHeight(28) btn_inv.setFixedHeight(28)
btn_inv.clicked.connect(self._invite) btn_inv.clicked.connect(self._invite)
row.addWidget(btn_inv) row.addWidget(btn_inv)
inv_layout.addLayout(row) inv_layout.addLayout(row)
note = QLabel("Brugeren får listen ved næste synkronisering.\nKun du kan redigere listen.")
note.setObjectName("result_count")
note.setWordWrap(True)
inv_layout.addWidget(note)
layout.addWidget(inv_frame) layout.addWidget(inv_frame)
# Liste over delinger # Liste
lbl = QLabel("Delt med:") lbl = QLabel("Delt med:")
lbl.setObjectName("track_meta") lbl.setObjectName("track_meta")
layout.addWidget(lbl) layout.addWidget(lbl)
self._shares_list = QListWidget() self._shares_list = QListWidget()
self._shares_list.setMaximumHeight(150) self._shares_list.setMaximumHeight(120)
layout.addWidget(self._shares_list) layout.addWidget(self._shares_list)
btn_remove = QPushButton("✕ Fjern valgt deling") btn_remove = QPushButton("✕ Fjern valgt deling")
@@ -116,6 +113,19 @@ class ShareDialog(QDialog):
except Exception: except Exception:
pass pass
def _set_visibility(self):
vis = self._vis_combo.currentData()
try:
import urllib.request, json
req = urllib.request.Request(
f"{self._server_url}/sharing/playlists/{self._playlist_id}/visibility?visibility={vis}",
data=b"", headers=self._headers(), method="PATCH"
)
urllib.request.urlopen(req, timeout=8)
self._status.setText(f"✓ Synlighed gemt")
except Exception as e:
self._status.setText(f"{e}")
def _load_shares(self): def _load_shares(self):
try: try:
import urllib.request, json import urllib.request, json
@@ -127,52 +137,30 @@ class ShareDialog(QDialog):
shares = json.loads(resp.read()) shares = json.loads(resp.read())
self._shares_list.clear() self._shares_list.clear()
for s in shares: for s in shares:
perm = {"view": "Se", "copy": "Kopiere", "edit": "Redigere"}.get( item = QListWidgetItem(s["email"])
s["permission"], s["permission"]
)
accepted = "" if s["accepted"] else ""
item = QListWidgetItem(f"{accepted} {s['email']}{perm}")
item.setData(Qt.ItemDataRole.UserRole, s["id"]) item.setData(Qt.ItemDataRole.UserRole, s["id"])
self._shares_list.addItem(item) self._shares_list.addItem(item)
except Exception as e: except Exception as e:
self._status.setText(f"Kunne ikke hente delinger: {e}") self._status.setText(f"Kunne ikke hente delinger: {e}")
def _set_visibility(self):
vis = self._vis_combo.currentData()
try:
import urllib.request, json
req = urllib.request.Request(
f"{self._server_url}/sharing/playlists/{self._playlist_id}/visibility?visibility={vis}",
data=b"",
headers=self._headers(),
method="PATCH"
)
with urllib.request.urlopen(req, timeout=8) as resp:
json.loads(resp.read())
self._status.setText(f"✓ Synlighed sat til {self._vis_combo.currentText()}")
except Exception as e:
self._status.setText(f"⚠ Fejl: {e}")
def _invite(self): def _invite(self):
email = self._email_input.text().strip() email = self._email_input.text().strip()
perm = self._perm_combo.currentData()
if not email or "@" not in email: if not email or "@" not in email:
self._status.setText("⚠ Ugyldig e-mailadresse") self._status.setText("⚠ Ugyldig e-mailadresse")
return return
try: try:
import urllib.request, json import urllib.request, json
data = json.dumps({"email": email, "permission": perm}).encode() data = json.dumps({"email": email}).encode()
req = urllib.request.Request( req = urllib.request.Request(
f"{self._server_url}/sharing/playlists/{self._playlist_id}/share", f"{self._server_url}/sharing/playlists/{self._playlist_id}/share",
data=data, headers=self._headers(), method="POST" data=data, headers=self._headers(), method="POST"
) )
with urllib.request.urlopen(req, timeout=8) as resp: urllib.request.urlopen(req, timeout=8)
json.loads(resp.read())
self._email_input.clear() self._email_input.clear()
self._status.setText(f"Invitation sendt til {email}") self._status.setText(f"Delt med {email}")
self._load_shares() self._load_shares()
except Exception as e: except Exception as e:
self._status.setText(f" Fejl: {e}") self._status.setText(f"{e}")
def _remove_share(self): def _remove_share(self):
item = self._shares_list.currentItem() item = self._shares_list.currentItem()
@@ -189,4 +177,4 @@ class ShareDialog(QDialog):
self._status.setText("✓ Deling fjernet") self._status.setText("✓ Deling fjernet")
self._load_shares() self._load_shares()
except Exception as e: except Exception as e:
self._status.setText(f" Fejl: {e}") self._status.setText(f"{e}")

View File

@@ -44,6 +44,25 @@ QPushButton#btn_play {
QPushButton#btn_play:hover { QPushButton#btn_play:hover {
background-color: #c47a10; background-color: #c47a10;
} }
QPushButton#btn_play_small {
background-color: #e8a020;
color: #111214;
border-color: #c47a10;
font-size: 14px;
font-weight: bold;
padding: 0px 0px;
}
QPushButton#btn_play_small:hover {
background-color: #c47a10;
}
QPushButton#btn_stop_small {
color: #e74c3c;
font-size: 12px;
padding: 0px 0px;
}
QPushButton#btn_stop_small:hover {
border-color: #e74c3c;
}
QPushButton#btn_stop { QPushButton#btn_stop {
color: #e74c3c; color: #e74c3c;
} }
@@ -301,6 +320,14 @@ QPushButton#btn_play {
color: #fff; color: #fff;
border-color: #a05808; border-color: #a05808;
} }
QPushButton#btn_play_small {
background-color: #c07010;
color: #fff;
border-color: #a05808;
}
QPushButton#btn_play_small:hover {
background-color: #a05808;
}
QListWidget { QListWidget {
background-color: #d8dae0; background-color: #d8dae0;
color: #1a1c22; color: #1a1c22;