Changeset d43ea3f in Klonkt
- Timestamp:
- 06/20/2026 04:22:32 AM (3 months ago)
- Branches:
- main
- Children:
- 71e8fa5
- Parents:
- b56afa7
- Location:
- src
- Files:
-
- 4 edited
-
assets/css/embed.css (modified) (1 diff)
-
assets/js/audio-player.js (modified) (7 diffs)
-
assets/js/embed-player.js (modified) (2 diffs)
-
views/shell.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/embed.css
rb56afa7 rd43ea3f 133 133 pointer-events: none; 134 134 } 135 /* YouTube-afspeellijst als album-kaart: geen losse embed-card-chrome, de 136 * .post-album-stijl (uit style.css) stuurt de weergave. */ 137 .pcms-ytalbum { background: none; border: 0; padding: 0; margin: 1.5rem 0; } 138 .pcms-ytalbum-msg { padding: .75rem 1rem; color: var(--ink-soft, #6b6560); font-size: .9rem; } 139 135 140 /* Audio-only embed-kaart die nu via de onderbalk speelt = launcher-hint. */ 136 141 .pcms-embed-elsewhere { opacity: .9; } -
src/assets/js/audio-player.js
rb56afa7 rd43ea3f 341 341 isPlaying = false; root.classList.remove('is-playing'); stopYTTick(); 342 342 } else if (s === 0) { 343 stopYTTick(); next(); 343 stopYTTick(); 344 // Bij een afspeellijst regelt YouTube zelf het doorschakelen → niet dubbel. 345 const t = queue[currentIndex]; 346 if (!(t && t.ytList)) next(); 344 347 } 345 348 } … … 356 359 function pullYTMeta() { 357 360 try { 358 const t = queue[currentIndex]; if (!t || !t.yt || !ytPlayer.getVideoData) return; 359 const vd = ytPlayer.getVideoData(); 360 if (vd && vd.title && (!t.title || t.title === 'YouTube')) { 361 t.title = vd.title; titleEl.textContent = vd.title; sheetTitle.textContent = vd.title; renderQueue(); markPlaying(t.id); 361 const t = queue[currentIndex]; if (!t || (!t.yt && !t.ytList) || !ytPlayer.getVideoData) return; 362 const vd = ytPlayer.getVideoData() || {}; 363 // Bij een afspeellijst wisselt de titel per nummer → altijd bijwerken. 364 if (vd.title && (t.ytList || !t.title || t.title === 'YouTube')) { 365 titleEl.textContent = vd.title; sheetTitle.textContent = vd.title; 366 if (!t.ytList) t.title = vd.title; 367 } 368 if (vd.video_id) { 369 setYtCover(vd.video_id); 370 markPlaying(vd.video_id); // licht de bijbehorende album-rij op (id="track-<videoId>") 362 371 } 363 372 } catch (e) {} … … 395 404 if (metaOnly) return; 396 405 397 // YouTube-bron → verborgen YT-speler i.p.v. de blob-<audio>.398 if (t.yt ) {406 // YouTube-bron (los nummer óf afspeellijst/album) → verborgen YT-speler. 407 if (t.yt || t.ytList) { 399 408 ytMode = true; 400 409 try { audio.pause(); } catch (e) {} … … 408 417 if (!p) { onLoadError(new Error('YT API niet beschikbaar'), loadSeq); return; } 409 418 try { 410 if (autoplay) p.loadVideoById(t.yt); else p.cueVideoById(t.yt); 419 if (t.ytList) { 420 const idx = t.ytIndex || 0; 421 if (autoplay) p.loadPlaylist({ list: t.ytList, index: idx }); else p.cuePlaylist({ list: t.ytList, index: idx }); 422 } else { 423 if (autoplay) p.loadVideoById(t.yt); else p.cueVideoById(t.yt); 424 } 411 425 p.setVolume(audio.muted ? 0 : Math.round(audio.volume * 100)); 412 426 } catch (e) {} … … 500 514 function togglePlay() { if (ytMode) { isPlaying ? pause() : play(); return; } audio.paused ? play() : pause(); } 501 515 function next() { 516 const t = queue[currentIndex]; 517 if (t && t.ytList && ytPlayer && ytMode) { try { ytPlayer.nextVideo(); } catch (e) {} return; } 502 518 if (!queue.length) return; 503 519 loadTrack((currentIndex + 1) % queue.length, true); 504 520 } 505 521 function prev() { 522 const t = queue[currentIndex]; 523 if (t && t.ytList && ytPlayer && ytMode) { try { ytPlayer.previousVideo(); } catch (e) {} return; } 506 524 if (!queue.length) return; 507 525 loadTrack(currentIndex === 0 ? queue.length - 1 : currentIndex - 1, true); … … 916 934 // 'ie in de onderbalk én speelt 'ie door bij navigeren. 917 935 function playYouTube(opts) { 918 if (!opts || !opts.id) return;936 if (!opts || (!opts.id && !opts.list)) return; 919 937 setQueue([{ 920 yt: opts.id, 938 yt: opts.id || null, 939 ytList: opts.list || null, // YouTube-afspeellijst/album (OLAK…/PL…) 940 ytIndex: opts.index || 0, // startindex binnen de lijst 921 941 title: opts.title || 'YouTube', 922 942 artist: opts.artist || '', … … 924 944 postUrl: opts.postUrl || '', 925 945 }], 0); 926 // Upgrade de cover async naar de scherpe maxresdefault (ook 16:9, geen balken) 927 // als die bestaat. Async → de user-gesture voor afspelen blijft intact; de 928 // direct gezette mqdefault toont al meteen een balkloze cover. 946 // De scherpe cover + echte titel komen via pullYTMeta() zodra het nummer speelt. 947 } 948 949 // Zet de bar-cover op de thumbnail van een video (mqdefault = balkloos), en 950 // upgrade async naar de scherpe maxresdefault als die bestaat. 951 function setYtCover(videoId) { 952 if (!videoId) return; 953 const mq = 'https://i.ytimg.com/vi/' + videoId + '/mqdefault.jpg'; 954 setCoverImage(cover, mq); setCoverImage(sheetCover, mq); 929 955 try { 930 const maxres = 'https://i.ytimg.com/vi/' + opts.id + '/maxresdefault.jpg';956 const maxres = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg'; 931 957 const im = new Image(); 932 958 im.onload = () => { 933 if (im.naturalWidth <= 120) return; // 120x90 = YouTube-placeholder (geen maxres)934 const t = queue[currentIndex];935 if ( t && t.yt === opts.id) { t.cover = maxres;setCoverImage(cover, maxres); setCoverImage(sheetCover, maxres); }959 if (im.naturalWidth <= 120) return; 960 const cur = ytPlayer && ytPlayer.getVideoData ? (ytPlayer.getVideoData() || {}).video_id : null; 961 if (cur === videoId) { setCoverImage(cover, maxres); setCoverImage(sheetCover, maxres); } 936 962 }; 937 963 im.src = maxres; -
src/assets/js/embed-player.js
rb56afa7 rd43ea3f 161 161 const url = el.dataset.embedUrl || ''; 162 162 if (!provider) return; 163 164 // Audio-only YouTube met een afspeellijst (?list=…) → album-kaart met tracklijst. 165 if (provider === 'youtube' && el.dataset.embedMode === 'audio') { 166 const lm = url.match(/[?&]list=([A-Za-z0-9_-]+)/); 167 if (lm) { try { buildYtAlbum(el, lm[1]); } catch (e) { console.error('[pcms-embed] yt-album faalde', e); } return; } 168 } 163 169 164 170 el.classList.add('pcms-embed-card', 'pcms-embed-card--' + provider); … … 381 387 function escAttr(s) { 382 388 return String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); 389 } 390 391 // Haal de video-IDs van een YouTube-afspeellijst op via een verborgen, 392 // tijdelijke speler (cuePlaylist → getPlaylist). Geen API-key nodig. 393 function ytPlaylistIds(listId) { 394 return ytApi().then((YT) => new Promise((resolve) => { 395 if (!YT || !YT.Player) { resolve([]); return; } 396 const host = document.createElement('div'); 397 host.style.cssText = 'position:absolute;left:-99999px;top:0;width:320px;height:180px;opacity:0;pointer-events:none'; 398 const mount = document.createElement('div'); host.appendChild(mount); document.body.appendChild(host); 399 let done = false; 400 const finish = (ids) => { if (done) return; done = true; try { p.destroy(); } catch (e) {} try { host.remove(); } catch (e) {} resolve(ids || []); }; 401 const p = new YT.Player(mount, { 402 host: 'https://www.youtube-nocookie.com', playerVars: { playsinline: 1 }, 403 events: { onReady: () => { 404 try { p.cuePlaylist({ list: listId }); } catch (e) {} 405 setTimeout(() => { let ids = []; try { ids = p.getPlaylist() || []; } catch (e) {} finish(ids); }, 2500); 406 } }, 407 }); 408 setTimeout(() => finish([]), 9000); 409 })); 410 } 411 412 // Render een audio-only YouTube-AFSPEELLIJST als album-kaart (zelfde stijl als 413 // de eigen albums) met tracklijst; afspelen loopt via de site-audiospeler. 414 async function buildYtAlbum(el, listId) { 415 el.className = 'folio-embed pcms-ytalbum'; // geen losse card-chrome; .post-album stuurt de stijl 416 const thumb = (id) => `https://i.ytimg.com/vi/${id}/mqdefault.jpg`; 417 el.innerHTML = '' 418 + '<div class="post-album">' 419 + '<div class="post-album-header">' 420 + '<button type="button" class="post-album-cover-btn" aria-label="Album afspelen">' 421 + '<img class="post-album-cover-img" src="" alt="">' 422 + '<span class="post-album-play-overlay" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 4l12 8-12 8z"/></svg></span>' 423 + '</button>' 424 + '<div class="post-album-info">' 425 + '<h3 class="post-album-title">Album laden…</h3>' 426 + '<p class="post-album-artist"></p>' 427 + '<p class="post-album-count">YouTube</p>' 428 + '</div>' 429 + '</div>' 430 + '<ol class="post-album-tracks"><li class="pcms-ytalbum-msg">Tracks laden…</li></ol>' 431 + '</div>'; 432 const coverImg = el.querySelector('.post-album-cover-img'); 433 const titleEl = el.querySelector('.post-album-title'); 434 const countEl = el.querySelector('.post-album-count'); 435 const olEl = el.querySelector('.post-album-tracks'); 436 const coverBtn = el.querySelector('.post-album-cover-btn'); 437 438 // Albumnaam via de oEmbed-proxy (playlist-url). 439 fetch('/audio/yt-title?url=' + encodeURIComponent('https://www.youtube.com/playlist?list=' + listId)) 440 .then((r) => (r.ok ? r.json() : null)).then((d) => { if (d && d.title) titleEl.textContent = d.title; }).catch(() => {}); 441 442 let ids = []; 443 try { ids = await ytPlaylistIds(listId); } catch (e) { ids = []; } 444 if (!ids.length) { olEl.innerHTML = '<li class="pcms-ytalbum-msg">Kon de afspeellijst niet laden.</li>'; return; } 445 countEl.textContent = ids.length + ' track' + (ids.length === 1 ? '' : 's'); 446 if (coverImg) coverImg.src = thumb(ids[0]); 447 448 olEl.innerHTML = ids.map((id, i) => 449 `<li class="post-album-track-compact" id="track-${id}" data-yt-id="${id}" data-yt-index="${i}">` 450 + `<button type="button" class="pat-row" aria-label="Speel track ${i + 1}">` 451 + `<span class="pat-cover" style="background-image:url('${thumb(id)}')" aria-hidden="true"></span>` 452 + `<span class="pat-meta"><span class="pat-title">Track ${i + 1}</span><span class="pat-artist"></span></span>` 453 + `<span class="pat-duration pat-duration-empty">—:—</span>` 454 + `</button></li>` 455 ).join(''); 456 457 // Tracktitels lazy invullen (oEmbed-proxy per video). 458 ids.forEach((id, i) => { 459 fetch('/audio/yt-title?url=' + encodeURIComponent('https://www.youtube.com/watch?v=' + id)) 460 .then((r) => (r.ok ? r.json() : null)) 461 .then((d) => { if (d && d.title) { const tt = olEl.querySelector(`[data-yt-index="${i}"] .pat-title`); if (tt) tt.textContent = d.title; } }) 462 .catch(() => {}); 463 }); 464 465 function playAt(i) { 466 if (!(window.pcmsAudioPlayer && window.pcmsAudioPlayer.playYouTube)) return; 467 const tt = (olEl.querySelector(`[data-yt-index="${i}"] .pat-title`) || {}).textContent || ''; 468 window.pcmsAudioPlayer.playYouTube({ list: listId, index: i, title: tt, cover: thumb(ids[i]), postUrl: location.pathname + location.search }); 469 } 470 coverBtn.addEventListener('click', () => playAt(0)); 471 olEl.addEventListener('click', (e) => { const li = e.target.closest('[data-yt-index]'); if (li) playAt(parseInt(li.dataset.ytIndex, 10) || 0); }); 383 472 } 384 473 -
src/views/shell.ejs
rb56afa7 rd43ea3f 170 170 <link rel="stylesheet" href="/assets/css/audio.css?v=6"> 171 171 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. --> 172 <link rel="stylesheet" href="/assets/css/embed.css?v= 6">172 <link rel="stylesheet" href="/assets/css/embed.css?v=7"> 173 173 174 174 <%- include('partials/shared-styles') %> … … 300 300 ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat 301 301 Cloudflare (max-age=1y) niet de oude versie blijft serveren. --> 302 <script src="/assets/js/audio-player.js?v=2 2"></script>302 <script src="/assets/js/audio-player.js?v=23"></script> 303 303 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte 304 304 player-API's + gedeelde mutual-exclusion registry met de site-speler. --> 305 <script src="/assets/js/embed-player.js?v=1 1" defer></script>305 <script src="/assets/js/embed-player.js?v=12" defer></script> 306 306 <% if (site && site.enable_audio_player && audioTracks && audioTracks.length > 0) { %> 307 307 <script>window.PCMS_SITE_TRACKS = <%- JSON.stringify(audioTracks) %>;</script>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)