Næster version
This commit is contained in:
@@ -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 sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
@@ -15,21 +15,9 @@ router = APIRouter(prefix="/sharing", tags=["sharing"])
|
||||
|
||||
class ShareRequest(BaseModel):
|
||||
email: EmailStr
|
||||
permission: str = "view" # view | copy | edit
|
||||
|
||||
|
||||
class ShareOut(BaseModel):
|
||||
id: str
|
||||
project_id: str
|
||||
invited_email: str
|
||||
permission: str
|
||||
accepted_at: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ── Del en playliste ──────────────────────────────────────────────────────────
|
||||
# ── Del med bruger ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/playlists/{project_id}/share", status_code=201)
|
||||
async def share_playlist(
|
||||
@@ -39,35 +27,27 @@ async def share_playlist(
|
||||
db: Session = Depends(get_db),
|
||||
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()
|
||||
if not project:
|
||||
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()
|
||||
|
||||
# Tjek om deling allerede eksisterer
|
||||
existing = db.query(PlaylistShare).filter_by(
|
||||
project_id=project_id,
|
||||
invited_email=data.email,
|
||||
project_id=project_id, invited_email=data.email
|
||||
).first()
|
||||
if existing:
|
||||
existing.permission = data.permission
|
||||
db.commit()
|
||||
return {"detail": "Rettigheder opdateret", "share_id": existing.id}
|
||||
return {"detail": "Allerede delt med denne bruger"}
|
||||
|
||||
share = PlaylistShare(
|
||||
project_id=project_id,
|
||||
shared_with_id=target.id if target else None,
|
||||
invited_email=data.email,
|
||||
permission=data.permission,
|
||||
permission="view",
|
||||
)
|
||||
db.add(share)
|
||||
db.commit()
|
||||
db.refresh(share)
|
||||
|
||||
# Send invitation-mail
|
||||
try:
|
||||
@@ -78,32 +58,13 @@ async def share_playlist(
|
||||
email=data.email,
|
||||
owner_name=me.username,
|
||||
playlist_name=project.name,
|
||||
permission=data.permission,
|
||||
accept_url=f"{settings.BASE_URL}/sharing/accept/{share.id}",
|
||||
permission="view",
|
||||
accept_url=f"{settings.BASE_URL}/sharing/playlists/{project_id}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"detail": "Invitation sendt", "share_id": share.id}
|
||||
|
||||
|
||||
@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"}
|
||||
return {"detail": f"Delt med {data.email}"}
|
||||
|
||||
|
||||
@router.delete("/playlists/{project_id}/share/{share_id}", status_code=204)
|
||||
@@ -133,15 +94,7 @@ def list_shares(
|
||||
if not project:
|
||||
raise HTTPException(404, "Playliste ikke fundet")
|
||||
shares = db.query(PlaylistShare).filter_by(project_id=project_id).all()
|
||||
return [
|
||||
{
|
||||
"id": s.id,
|
||||
"email": s.invited_email,
|
||||
"permission": s.permission,
|
||||
"accepted": s.accepted_at is not None,
|
||||
}
|
||||
for s in shares
|
||||
]
|
||||
return [{"id": s.id, "email": s.invited_email} for s in shares]
|
||||
|
||||
|
||||
# ── Visibility ────────────────────────────────────────────────────────────────
|
||||
@@ -154,64 +107,16 @@ def set_visibility(
|
||||
me: User = Depends(get_current_user),
|
||||
):
|
||||
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()
|
||||
if not project:
|
||||
raise HTTPException(404, "Playliste ikke fundet")
|
||||
project.visibility = visibility
|
||||
db.commit()
|
||||
return {"detail": f"Synlighed sat til {visibility}"}
|
||||
return {"detail": f"Synlighed: {visibility}"}
|
||||
|
||||
|
||||
# ── Hent delte lister ─────────────────────────────────────────────────────────
|
||||
|
||||
@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 ────────────────────────────────────────────────────
|
||||
# ── Hent playliste-indhold ────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/playlists/{project_id}")
|
||||
def get_shared_playlist(
|
||||
@@ -219,12 +124,9 @@ def get_shared_playlist(
|
||||
db: Session = Depends(get_db),
|
||||
me: User = Depends(get_current_user),
|
||||
):
|
||||
"""Hent indholdet af en delt playliste."""
|
||||
p = db.query(Project).filter_by(id=project_id).first()
|
||||
if not p:
|
||||
raise HTTPException(404, "Playliste ikke fundet")
|
||||
|
||||
# Tjek adgang
|
||||
if p.owner_id != me.id:
|
||||
if p.visibility != "public":
|
||||
share = db.query(PlaylistShare).filter(
|
||||
@@ -233,7 +135,7 @@ def get_shared_playlist(
|
||||
(PlaylistShare.invited_email == me.email)
|
||||
).first()
|
||||
if not share:
|
||||
raise HTTPException(403, "Du har ikke adgang til denne playliste")
|
||||
raise HTTPException(403, "Ingen adgang")
|
||||
|
||||
from app.models import Song
|
||||
songs = []
|
||||
@@ -244,151 +146,16 @@ def get_shared_playlist(
|
||||
songs.append({
|
||||
"title": song.title,
|
||||
"artist": song.artist,
|
||||
"album": song.album,
|
||||
"bpm": song.bpm,
|
||||
"duration_sec": song.duration_sec,
|
||||
"position": ps.position,
|
||||
"status": ps.status,
|
||||
"is_workshop": ps.is_workshop,
|
||||
"dance_override": ps.dance_override,
|
||||
"dance_override": ps.dance_override or "",
|
||||
})
|
||||
|
||||
return {
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"description": p.description,
|
||||
"description": p.description or "",
|
||||
"visibility": p.visibility,
|
||||
"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)}
|
||||
|
||||
@@ -268,28 +268,37 @@ def pull(
|
||||
"dance_id": cd.dance_id,
|
||||
})
|
||||
|
||||
# Delte playlister
|
||||
shared_ids = [
|
||||
s.project_id for s in
|
||||
db.query(PlaylistShare).filter_by(shared_with_id=me.id).all()
|
||||
]
|
||||
# Delte playlister (read-only — kun ejeren kan redigere)
|
||||
shared_ids = set()
|
||||
for s in db.query(PlaylistShare).filter(
|
||||
(PlaylistShare.shared_with_id == me.id) |
|
||||
(PlaylistShare.invited_email == me.email)
|
||||
).all():
|
||||
shared_ids.add(s.project_id)
|
||||
|
||||
shared = []
|
||||
for p in db.query(Project).filter(Project.id.in_(shared_ids)).all():
|
||||
shared.append({
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"owner_id": p.owner_id,
|
||||
"visibility": p.visibility,
|
||||
"songs": [
|
||||
{
|
||||
"song_id": ps.song_id,
|
||||
if p.owner_id == me.id:
|
||||
continue # Egne lister håndteres separat
|
||||
owner = db.query(User).filter_by(id=p.owner_id).first()
|
||||
songs_out = []
|
||||
for ps in p.project_songs:
|
||||
song = db.query(Song).filter_by(id=ps.song_id).first()
|
||||
if not song:
|
||||
continue
|
||||
songs_out.append({
|
||||
"title": song.title,
|
||||
"artist": song.artist,
|
||||
"position": ps.position,
|
||||
"status": ps.status,
|
||||
"is_workshop": ps.is_workshop,
|
||||
"dance_override": ps.dance_override,
|
||||
}
|
||||
for ps in p.project_songs
|
||||
]
|
||||
"dance_override": ps.dance_override or "",
|
||||
})
|
||||
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
|
||||
|
||||
@@ -4,7 +4,6 @@ Paa Windows uden konsol skrives alt til ~/.linedance/app.log
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
LOG_PATH = Path.home() / ".linedance" / "app.log"
|
||||
@@ -13,12 +12,6 @@ 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")]
|
||||
if sys.stdout and hasattr(sys.stdout, 'write'):
|
||||
try:
|
||||
sys.stdout.write("")
|
||||
handlers.append(logging.StreamHandler(sys.stdout))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
|
||||
@@ -312,5 +312,59 @@ class SyncManager:
|
||||
song_data.get("dance_override","") or ""))
|
||||
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.close()
|
||||
|
||||
@@ -216,9 +216,38 @@ class Player(QObject):
|
||||
|
||||
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):
|
||||
"""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
|
||||
_QTimer.singleShot(0, self._handle_end_in_main_thread)
|
||||
|
||||
@@ -227,3 +256,77 @@ class Player(QObject):
|
||||
self._poll_timer.stop()
|
||||
self.song_ended.emit()
|
||||
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)
|
||||
|
||||
@@ -5,6 +5,7 @@ library_panel.py — Musikbibliotek med søgning og drag-and-drop til danseliste
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
||||
QLineEdit, QLabel, QHBoxLayout, QPushButton,
|
||||
QFrame, QSlider, QCheckBox,
|
||||
QAbstractItemView, QStyledItemDelegate,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray, QRect
|
||||
@@ -175,6 +176,73 @@ class LibraryPanel(QWidget):
|
||||
self._list.setItemDelegate(DanseButtonDelegate(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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _on_scan_clicked(self):
|
||||
@@ -312,11 +380,6 @@ class LibraryPanel(QWidget):
|
||||
|
||||
# ── 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):
|
||||
from PyQt6.QtWidgets import QMenu
|
||||
item = self._list.itemAt(pos)
|
||||
@@ -328,6 +391,15 @@ class LibraryPanel(QWidget):
|
||||
menu = QMenu(self)
|
||||
act_add = menu.addAction("Tilføj til danseliste")
|
||||
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()
|
||||
act_tags = menu.addAction("✎ Rediger dans-tags...")
|
||||
act_info = menu.addAction("ℹ Dans-info...")
|
||||
@@ -340,6 +412,8 @@ class LibraryPanel(QWidget):
|
||||
self.add_to_playlist.emit(song)
|
||||
elif action == act_play:
|
||||
self.song_selected.emit(song)
|
||||
elif action == act_preview:
|
||||
self._toggle_preview(song)
|
||||
elif action == act_tags:
|
||||
self.edit_tags_requested.emit(song)
|
||||
elif action == act_info:
|
||||
@@ -351,6 +425,78 @@ class LibraryPanel(QWidget):
|
||||
elif action == act_mail:
|
||||
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):
|
||||
"""Analysér BPM i baggrundstråd og opdater biblioteket."""
|
||||
path = song.get("local_path", "")
|
||||
|
||||
@@ -76,6 +76,9 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._dark_theme = True
|
||||
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._song_ended = False
|
||||
self._demo_active = False
|
||||
@@ -349,6 +352,15 @@ class MainWindow(QMainWindow):
|
||||
self._sync_debounce.timeout.connect(self._auto_sync)
|
||||
|
||||
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.add_to_playlist.connect(self._add_song_to_playlist)
|
||||
self._library_panel.scan_requested.connect(self.start_scan)
|
||||
@@ -628,6 +640,13 @@ class MainWindow(QMainWindow):
|
||||
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._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):
|
||||
"""Forsøg automatisk login med gemte oplysninger."""
|
||||
@@ -970,6 +989,10 @@ class MainWindow(QMainWindow):
|
||||
dur = song.get("duration_sec", 0)
|
||||
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", "—"))
|
||||
bpm = song.get("bpm", 0)
|
||||
fmt_dur = f"{dur//60}:{dur%60:02d}"
|
||||
@@ -1006,6 +1029,7 @@ class MainWindow(QMainWindow):
|
||||
self._btn_demo.setChecked(False)
|
||||
self._btn_play.setText("▶")
|
||||
return
|
||||
self._waiting_for_auto = False # annuller evt. auto-timer
|
||||
if self._player.is_playing():
|
||||
self._player.pause()
|
||||
else:
|
||||
@@ -1014,6 +1038,13 @@ class MainWindow(QMainWindow):
|
||||
self._btn_play.setText("⏸")
|
||||
|
||||
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._song_ended = False
|
||||
self._demo_active = False
|
||||
@@ -1104,7 +1135,27 @@ class MainWindow(QMainWindow):
|
||||
self._current_idx = ni
|
||||
self._playlist_panel.set_next_ready(ni)
|
||||
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")
|
||||
|
||||
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:
|
||||
self._current_idx = -1
|
||||
self._playlist_panel._current_idx = -1
|
||||
@@ -1116,6 +1167,44 @@ class MainWindow(QMainWindow):
|
||||
self._lbl_dances.setText("")
|
||||
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):
|
||||
"""Gem event-fremgang (afspillet/sprunget over) til den navngivne liste."""
|
||||
try:
|
||||
|
||||
@@ -196,6 +196,11 @@ class PlaylistPanel(QWidget):
|
||||
|
||||
def set_next_ready(self, idx: int):
|
||||
"""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._song_ended = False
|
||||
# Ændr KUN status hvis den er pending — rør ikke skipped/played
|
||||
@@ -427,7 +432,11 @@ class PlaylistPanel(QWidget):
|
||||
|
||||
if 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._current_idx = -1
|
||||
self._song_ended = False
|
||||
@@ -440,7 +449,6 @@ class PlaylistPanel(QWidget):
|
||||
ni = self.next_playable_idx()
|
||||
if ni is not None:
|
||||
self._current_idx = ni
|
||||
self._statuses[ni] = "playing"
|
||||
|
||||
self._refresh()
|
||||
return True
|
||||
|
||||
@@ -24,6 +24,10 @@ SETTINGS_KEY_SERVER_URL = "online/server_url"
|
||||
SETTINGS_KEY_LANGUAGE = "appearance/language"
|
||||
SETTINGS_KEY_BETWEEN_SEC = "playback/between_seconds"
|
||||
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:
|
||||
@@ -42,6 +46,10 @@ def load_settings() -> dict:
|
||||
"language": s.value(SETTINGS_KEY_LANGUAGE, "da"),
|
||||
"between_seconds": s.value(SETTINGS_KEY_BETWEEN_SEC, 60, 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_BETWEEN_SEC, values.get("between_seconds", 60))
|
||||
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):
|
||||
@@ -191,6 +203,63 @@ class SettingsDialog(QDialog):
|
||||
grp2_layout.addRow("Tid per workshop:", self._spin_workshop)
|
||||
|
||||
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()
|
||||
return tab
|
||||
|
||||
@@ -359,6 +428,28 @@ class SettingsDialog(QDialog):
|
||||
self._user_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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _save_and_close(self):
|
||||
@@ -375,6 +466,14 @@ class SettingsDialog(QDialog):
|
||||
"password": self._pass_input.text(),
|
||||
"server_url": self._server_url.text().strip() or "http://localhost:8000",
|
||||
"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)
|
||||
self._values = values
|
||||
|
||||
@@ -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 (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
@@ -19,7 +20,7 @@ class ShareDialog(QDialog):
|
||||
self._token = token
|
||||
|
||||
self.setWindowTitle(f"Del — {playlist_name}")
|
||||
self.setMinimumWidth(480)
|
||||
self.setMinimumWidth(440)
|
||||
self._build_ui()
|
||||
self._load_shares()
|
||||
self._load_visibility()
|
||||
@@ -42,9 +43,9 @@ class ShareDialog(QDialog):
|
||||
vis_layout.setContentsMargins(10, 8, 10, 8)
|
||||
vis_layout.addWidget(QLabel("Synlighed:"))
|
||||
self._vis_combo = QComboBox()
|
||||
self._vis_combo.addItem("🔒 Privat (kun mig)", "private")
|
||||
self._vis_combo.addItem("👥 Delt (inviterede)", "shared")
|
||||
self._vis_combo.addItem("🌐 Public (alle kan se)", "public")
|
||||
self._vis_combo.addItem("Privat — kun dig", "private")
|
||||
self._vis_combo.addItem("Delt — kun inviterede", "shared")
|
||||
self._vis_combo.addItem("Public — hjemmesiden", "public")
|
||||
vis_layout.addWidget(self._vis_combo, stretch=1)
|
||||
btn_vis = QPushButton("Gem")
|
||||
btn_vis.setFixedHeight(28)
|
||||
@@ -52,38 +53,34 @@ class ShareDialog(QDialog):
|
||||
vis_layout.addWidget(btn_vis)
|
||||
layout.addWidget(vis_frame)
|
||||
|
||||
# Invitér bruger
|
||||
# Del med bruger
|
||||
inv_frame = QFrame()
|
||||
inv_frame.setObjectName("track_display")
|
||||
inv_layout = QVBoxLayout(inv_frame)
|
||||
inv_layout.setContentsMargins(10, 8, 10, 8)
|
||||
inv_layout.setSpacing(6)
|
||||
inv_layout.addWidget(QLabel("Invitér via e-mail:"))
|
||||
|
||||
inv_layout.addWidget(QLabel("Del med (e-mail):"))
|
||||
row = QHBoxLayout()
|
||||
self._email_input = QLineEdit()
|
||||
self._email_input.setPlaceholderText("bruger@eksempel.dk")
|
||||
row.addWidget(self._email_input)
|
||||
self._perm_combo = QComboBox()
|
||||
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 = QPushButton("Del")
|
||||
btn_inv.setFixedHeight(28)
|
||||
btn_inv.clicked.connect(self._invite)
|
||||
row.addWidget(btn_inv)
|
||||
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)
|
||||
|
||||
# Liste over delinger
|
||||
# Liste
|
||||
lbl = QLabel("Delt med:")
|
||||
lbl.setObjectName("track_meta")
|
||||
layout.addWidget(lbl)
|
||||
|
||||
self._shares_list = QListWidget()
|
||||
self._shares_list.setMaximumHeight(150)
|
||||
self._shares_list.setMaximumHeight(120)
|
||||
layout.addWidget(self._shares_list)
|
||||
|
||||
btn_remove = QPushButton("✕ Fjern valgt deling")
|
||||
@@ -116,6 +113,19 @@ class ShareDialog(QDialog):
|
||||
except Exception:
|
||||
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):
|
||||
try:
|
||||
import urllib.request, json
|
||||
@@ -127,52 +137,30 @@ class ShareDialog(QDialog):
|
||||
shares = json.loads(resp.read())
|
||||
self._shares_list.clear()
|
||||
for s in shares:
|
||||
perm = {"view": "Se", "copy": "Kopiere", "edit": "Redigere"}.get(
|
||||
s["permission"], s["permission"]
|
||||
)
|
||||
accepted = "✓" if s["accepted"] else "⏳"
|
||||
item = QListWidgetItem(f"{accepted} {s['email']} — {perm}")
|
||||
item = QListWidgetItem(s["email"])
|
||||
item.setData(Qt.ItemDataRole.UserRole, s["id"])
|
||||
self._shares_list.addItem(item)
|
||||
except Exception as 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):
|
||||
email = self._email_input.text().strip()
|
||||
perm = self._perm_combo.currentData()
|
||||
if not email or "@" not in email:
|
||||
self._status.setText("⚠ Ugyldig e-mailadresse")
|
||||
return
|
||||
try:
|
||||
import urllib.request, json
|
||||
data = json.dumps({"email": email, "permission": perm}).encode()
|
||||
data = json.dumps({"email": email}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{self._server_url}/sharing/playlists/{self._playlist_id}/share",
|
||||
data=data, headers=self._headers(), method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=8) as resp:
|
||||
json.loads(resp.read())
|
||||
urllib.request.urlopen(req, timeout=8)
|
||||
self._email_input.clear()
|
||||
self._status.setText(f"✓ Invitation sendt til {email}")
|
||||
self._status.setText(f"✓ Delt med {email}")
|
||||
self._load_shares()
|
||||
except Exception as e:
|
||||
self._status.setText(f"⚠ Fejl: {e}")
|
||||
self._status.setText(f"⚠ {e}")
|
||||
|
||||
def _remove_share(self):
|
||||
item = self._shares_list.currentItem()
|
||||
@@ -189,4 +177,4 @@ class ShareDialog(QDialog):
|
||||
self._status.setText("✓ Deling fjernet")
|
||||
self._load_shares()
|
||||
except Exception as e:
|
||||
self._status.setText(f"⚠ Fejl: {e}")
|
||||
self._status.setText(f"⚠ {e}")
|
||||
|
||||
@@ -44,6 +44,25 @@ QPushButton#btn_play {
|
||||
QPushButton#btn_play:hover {
|
||||
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 {
|
||||
color: #e74c3c;
|
||||
}
|
||||
@@ -301,6 +320,14 @@ QPushButton#btn_play {
|
||||
color: #fff;
|
||||
border-color: #a05808;
|
||||
}
|
||||
QPushButton#btn_play_small {
|
||||
background-color: #c07010;
|
||||
color: #fff;
|
||||
border-color: #a05808;
|
||||
}
|
||||
QPushButton#btn_play_small:hover {
|
||||
background-color: #a05808;
|
||||
}
|
||||
QListWidget {
|
||||
background-color: #d8dae0;
|
||||
color: #1a1c22;
|
||||
|
||||
Reference in New Issue
Block a user