source: Klonkt/src/views/pages/embed-player.ejs@ 19a72df

main
Last change on this file since 19a72df was 6be57b4, checked in by roboburr <roboburr@…>, 3 months ago

Embeddable player (premium feature #7)

Standalone /embed page (no shell) with a compact audio player for the site
tracks, intended for an <iframe> on external sites. The page runs on the klonkt
origin, so audio requests from the iframe remain same-origin -> the
/audio/stream gate lets them through, even on an external site. Helmet's
X-Frame-Options + frame-ancestors are overridden per route (frame-ancestors *)
so external embedding is allowed.

  • routes/embed.js (premium-gated): GET /embed.
  • views/pages/embed-player.ejs: standalone HTML, inline css/js, play/pause, track click, auto-next, progress bar; themable on site.accent.
  • admin-audio: embedUrl + "Embeddable player" section with copyable iframe code + preview link (premium).

node --check passed.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 5.3 KB
Line 
1<!DOCTYPE html>
2<html lang="nl">
3<head>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title><%= site.title %> — speler</title>
7<style>
8 :root { --ac: <%= (site && site.accent) ? site.accent : '#6b8f71' %>; }
9 * { box-sizing: border-box; }
10 html, body { margin: 0; }
11 body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #14161a; color: #f2f3f5; }
12 .ep { display: flex; flex-direction: column; height: 100vh; }
13 .ep-now { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,.08); }
14 .ep-play { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 50%; border: none; background: var(--ac); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; }
15 .ep-meta { flex: 1 1 auto; min-width: 0; }
16 .ep-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
17 .ep-artist { font-size: 12px; opacity: .6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
18 .ep-bar { height: 4px; background: rgba(255,255,255,.12); cursor: pointer; }
19 .ep-fill { height: 100%; width: 0; background: var(--ac); }
20 .ep-list { flex: 1 1 auto; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
21 .ep-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,.05); }
22 .ep-item:hover { background: rgba(255,255,255,.05); }
23 .ep-item.on { color: var(--ac); }
24 .ep-num { width: 20px; text-align: right; opacity: .5; font-size: 13px; font-variant-numeric: tabular-nums; }
25 .ep-it-title { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
26 .ep-dur { opacity: .5; font-size: 12px; font-variant-numeric: tabular-nums; }
27 .ep-foot { padding: 8px 14px; font-size: 11px; opacity: .5; text-align: center; border-top: 1px solid rgba(255,255,255,.08); }
28 .ep-foot a { color: inherit; }
29 .ep-empty { padding: 24px; text-align: center; opacity: .6; }
30</style>
31</head>
32<body>
33<div class="ep">
34 <div class="ep-now">
35 <button class="ep-play" id="ep-play" aria-label="Afspelen">▶</button>
36 <div class="ep-meta">
37 <div class="ep-title" id="ep-title"><%= site.title %></div>
38 <div class="ep-artist" id="ep-artist">Selecteer een nummer</div>
39 </div>
40 </div>
41 <div class="ep-bar" id="ep-bar"><div class="ep-fill" id="ep-fill"></div></div>
42 <% if (!embedTracks.length) { %>
43 <div class="ep-empty">Geen nummers beschikbaar.</div>
44 <% } else { %>
45 <ul class="ep-list" id="ep-list">
46 <% embedTracks.forEach(function(t, i){ %>
47 <li class="ep-item" data-i="<%= i %>">
48 <span class="ep-num"><%= i+1 %></span>
49 <span class="ep-it-title"><%= t.title %><% if (t.artist) { %> — <span style="opacity:.6"><%= t.artist %></span><% } %></span>
50 <span class="ep-dur" data-dur="<%= t.duration || 0 %>"></span>
51 </li>
52 <% }); %>
53 </ul>
54 <% } %>
55 <div class="ep-foot">via <a href="<%= siteUrlBase %>/" target="_blank" rel="noopener"><%= site.title %></a> · Klonkt</div>
56</div>
57<audio id="ep-audio" preload="none" playsinline></audio>
58<script>
59(function(){
60 var TRACKS = <%- JSON.stringify(embedTracks) %>;
61 var audio = document.getElementById('ep-audio');
62 var playBtn = document.getElementById('ep-play');
63 var titleEl = document.getElementById('ep-title');
64 var artistEl = document.getElementById('ep-artist');
65 var fill = document.getElementById('ep-fill');
66 var bar = document.getElementById('ep-bar');
67 var list = document.getElementById('ep-list');
68 var cur = -1;
69 function fmt(s){ s = parseInt(s,10)||0; var m=Math.floor(s/60), x=s%60; return m+':'+String(x).padStart(2,'0'); }
70 // Vul de duur-labels.
71 document.querySelectorAll('.ep-dur').forEach(function(el){ var d=parseInt(el.getAttribute('data-dur'),10)||0; if(d) el.textContent=fmt(d); });
72 function mark(){ document.querySelectorAll('.ep-item').forEach(function(li){ li.classList.toggle('on', parseInt(li.getAttribute('data-i'),10)===cur); }); }
73 function load(i, play){
74 if(i<0||i>=TRACKS.length) return;
75 cur=i; var t=TRACKS[i];
76 titleEl.textContent=t.title||'Naamloos';
77 artistEl.textContent=t.artist||'';
78 audio.src=t.url;
79 mark();
80 if(play){ audio.play().catch(function(){}); }
81 }
82 playBtn.addEventListener('click', function(){
83 if(cur<0){ load(0, true); return; }
84 if(audio.paused) audio.play().catch(function(){}); else audio.pause();
85 });
86 if(list) list.addEventListener('click', function(e){
87 var li=e.target.closest('.ep-item'); if(!li) return;
88 load(parseInt(li.getAttribute('data-i'),10), true);
89 });
90 audio.addEventListener('play', function(){ playBtn.textContent='⏸'; });
91 audio.addEventListener('pause', function(){ playBtn.textContent='▶'; });
92 audio.addEventListener('timeupdate', function(){ if(audio.duration) fill.style.width=(audio.currentTime/audio.duration*100)+'%'; });
93 audio.addEventListener('ended', function(){ if(cur+1<TRACKS.length) load(cur+1, true); else { playBtn.textContent='▶'; fill.style.width='0'; } });
94 bar.addEventListener('click', function(e){ if(!audio.duration) return; var r=bar.getBoundingClientRect(); audio.currentTime=(e.clientX-r.left)/r.width*audio.duration; });
95})();
96</script>
97</body>
98</html>
Note: See TracBrowser for help on using the repository browser.