Compare commits
3 Commits
a1b9c0139d
...
c6d66e66c6
| Author | SHA1 | Date | |
|---|---|---|---|
| c6d66e66c6 | |||
| 808ddd889d | |||
| 94cda7293f |
@@ -84,6 +84,11 @@ def _require_admin(request: Request):
|
|||||||
|
|
||||||
# ── Login ──────────────────────────────────────────────────────────────────────
|
# ── Login ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/status")
|
||||||
|
def admin_status(request: Request):
|
||||||
|
return {"logged_in": bool(request.session.get("admin_logged_in"))}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/login", response_class=HTMLResponse)
|
@router.get("/login", response_class=HTMLResponse)
|
||||||
def login_page(request: Request):
|
def login_page(request: Request):
|
||||||
return templates.TemplateResponse("admin/login.html", {"request": request, "error": None})
|
return templates.TemplateResponse("admin/login.html", {"request": request, "error": None})
|
||||||
|
|||||||
@@ -74,6 +74,8 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="td-desc">{{ pl.description[:80] }}{% if pl.description | length > 80 %}…{% endif %}</td>
|
<td class="td-desc">{{ pl.description[:80] }}{% if pl.description | length > 80 %}…{% endif %}</td>
|
||||||
<td class="td-actions">
|
<td class="td-actions">
|
||||||
|
<button class="btn-sm btn-play-now" id="play-btn-{{ pl.id }}"
|
||||||
|
onclick="playNow('{{ pl.spotify_id }}', '{{ pl.spotify_type }}', {{ pl.id }})">▶ Reproducir</button>
|
||||||
<button class="btn-sm" onclick="toggleEdit({{ pl.id }})">✏️ Editar</button>
|
<button class="btn-sm" onclick="toggleEdit({{ pl.id }})">✏️ Editar</button>
|
||||||
<form method="post" action="/admin/playlists/{{ pl.id }}/delete"
|
<form method="post" action="/admin/playlists/{{ pl.id }}/delete"
|
||||||
onsubmit="return confirm('¿Eliminar {{ pl.name }}?')">
|
onsubmit="return confirm('¿Eliminar {{ pl.name }}?')">
|
||||||
@@ -133,6 +135,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
async function playNow(spotifyId, spotifyType, plId) {
|
||||||
|
const btn = document.getElementById('play-btn-' + plId);
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⏳';
|
||||||
|
|
||||||
|
const body = spotifyType === 'track'
|
||||||
|
? { uris: [`spotify:track:${spotifyId}`] }
|
||||||
|
: { context_uri: `spotify:${spotifyType}:${spotifyId}` };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/player/play', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
btn.textContent = res.ok ? '✓ Reproduciendo' : '✗ Error';
|
||||||
|
} catch (_) {
|
||||||
|
btn.textContent = '✗ Error';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Reproducir'; }, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
function toggleEdit(id) {
|
function toggleEdit(id) {
|
||||||
const row = document.getElementById('edit-row-' + id);
|
const row = document.getElementById('edit-row-' + id);
|
||||||
const visible = row.style.display !== 'none';
|
const visible = row.style.display !== 'none';
|
||||||
@@ -149,6 +174,8 @@ function clearEmoji(id) {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.td-actions { display: flex; gap: .4rem; align-items: center; }
|
.td-actions { display: flex; gap: .4rem; align-items: center; }
|
||||||
|
.btn-play-now { color: var(--green); border-color: rgba(29,185,84,.4); }
|
||||||
|
.btn-play-now:hover { background: rgba(29,185,84,.12); }
|
||||||
|
|
||||||
.edit-row td { background: var(--surface2); padding: .75rem 1rem; }
|
.edit-row td { background: var(--surface2); padding: .75rem 1rem; }
|
||||||
.edit-form { display: flex; flex-direction: column; gap: .75rem; }
|
.edit-form { display: flex; flex-direction: column; gap: .75rem; }
|
||||||
|
|||||||
+10
-2
@@ -11,11 +11,19 @@
|
|||||||
<div class="brand">🎵 Cantina</div>
|
<div class="brand">🎵 Cantina</div>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="/">Reproductor</a>
|
<a href="/">Reproductor</a>
|
||||||
<a href="/admin/playlists">Playlists</a>
|
|
||||||
<a href="/admin/voting">Votación</a>
|
|
||||||
<a href="/stats/">Estadísticas</a>
|
<a href="/stats/">Estadísticas</a>
|
||||||
|
<a id="admin-nav-btn" href="/admin/login" class="btn-sm">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<script>
|
||||||
|
fetch('/admin/status').then(r => r.json()).then(data => {
|
||||||
|
const btn = document.getElementById('admin-nav-btn');
|
||||||
|
if (data.logged_in) {
|
||||||
|
btn.textContent = '⚙ Admin';
|
||||||
|
btn.href = '/admin/playlists';
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
</script>
|
||||||
<main>
|
<main>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -243,9 +243,18 @@ async function fetchVotingStatus() {
|
|||||||
const res = await fetch('/voting/status');
|
const res = await fetch('/voting/status');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
const wasOpen = votingOpen;
|
||||||
votingOpen = data.is_open;
|
votingOpen = data.is_open;
|
||||||
myVotedPlaylistId = data.my_last_vote_playlist_id;
|
myVotedPlaylistId = data.my_last_vote_playlist_id;
|
||||||
|
|
||||||
|
if (wasOpen && !votingOpen && data.playlists?.length) {
|
||||||
|
const totalVotes = data.playlists.reduce((s, p) => s + p.votes, 0);
|
||||||
|
if (totalVotes > 0) {
|
||||||
|
const winner = data.playlists.reduce((best, p) => p.votes > best.votes ? p : best);
|
||||||
|
playItem(winner.spotify_id, winner.spotify_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Solo sincronizar cooldown desde servidor si no hay timer local activo
|
// Solo sincronizar cooldown desde servidor si no hay timer local activo
|
||||||
if (cooldownRemaining <= 0 && data.cooldown_remaining > 0) {
|
if (cooldownRemaining <= 0 && data.cooldown_remaining > 0) {
|
||||||
startCooldownTick(data.cooldown_remaining);
|
startCooldownTick(data.cooldown_remaining);
|
||||||
@@ -291,9 +300,12 @@ function renderPlaylists(playlists, isVoting, myVote) {
|
|||||||
const btnLabel = cooldownRemaining > 0 ? `⏳ ${cooldownRemaining}s` : '👍 Votar';
|
const btnLabel = cooldownRemaining > 0 ? `⏳ ${cooldownRemaining}s` : '👍 Votar';
|
||||||
const btnDisabled = cooldownRemaining > 0 ? 'disabled' : '';
|
const btnDisabled = cooldownRemaining > 0 ? 'disabled' : '';
|
||||||
return `
|
return `
|
||||||
<div class="playlist-card clickable voting-card ${isVoted ? 'voted' : ''}">
|
<div class="playlist-card voting-card ${isVoted ? 'voted' : ''}">
|
||||||
${img}
|
<div class="voting-card-top clickable" onclick='openTracksModal(${JSON.stringify(pl)})'>
|
||||||
<div class="playlist-card-name">${pl.name}</div>
|
${img}
|
||||||
|
<div class="playlist-card-name">${pl.name}</div>
|
||||||
|
${pl.description ? `<div class="playlist-card-desc">${pl.description}</div>` : ''}
|
||||||
|
</div>
|
||||||
<div class="vote-bar-wrap"><div class="vote-bar" style="width:${pct}%"></div></div>
|
<div class="vote-bar-wrap"><div class="vote-bar" style="width:${pct}%"></div></div>
|
||||||
<div class="vote-info">
|
<div class="vote-info">
|
||||||
<span class="vote-count">${pl.votes} votos</span>
|
<span class="vote-count">${pl.votes} votos</span>
|
||||||
@@ -416,7 +428,18 @@ setInterval(fetchVotingStatus, 5000);
|
|||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voting-card { cursor: pointer; position: relative; }
|
.voting-card { position: relative; }
|
||||||
|
.voting-card-top { cursor: pointer; flex: 1; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
|
||||||
|
.voting-card-top:hover .playlist-card-name { color: var(--green); }
|
||||||
|
.playlist-card-desc {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.voting-card.voted { border: 2px solid var(--green); }
|
.voting-card.voted { border: 2px solid var(--green); }
|
||||||
|
|
||||||
.vote-bar-wrap {
|
.vote-bar-wrap {
|
||||||
|
|||||||
+2
-1
@@ -197,8 +197,9 @@ a:hover { text-decoration: underline; }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
.playlist-thumb-emoji { font-size: 2.4rem; }
|
.playlist-thumb-emoji { font-size: 55cqi; }
|
||||||
|
|
||||||
.playlist-card-name {
|
.playlist-card-name {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user