Changeset 834bcc3 in Klonkt for src/assets
- Timestamp:
- 06/23/2026 06:14:27 PM (3 months ago)
- Branches:
- main
- Children:
- d774679
- Parents:
- bb42dfb
- Location:
- src/assets/js
- Files:
-
- 3 edited
-
audio-player.js (modified) (23 diffs)
-
embed-player.js (modified) (27 diffs)
-
smooth-scroll.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/audio-player.js
rbb42dfb r834bcc3 230 230 } 231 231 currentObjectUrl = objUrl; 232 // Schone overgang: pause + load forceert reset van internal state na233 // m eerdere src-changes (voorkomt state-corruption van het audio-element).232 // Clean transition: pause + load forces a reset of internal state after 233 // multiple src changes (prevents state corruption of the audio element). 234 234 try { audio.pause(); } catch (e) {} 235 235 audio.src = objUrl; … … 276 276 // Used by the site pre-seed so opening a page doesn't auto-download audio; 277 277 // the blob is fetched lazily on the first play(). 278 // Markeer de huidige track persistent (blijvende highlight zolang 'ie actief is).278 // Persistently mark the current track (stays highlighted as long as it's active). 279 279 function markPlaying(trackId) { 280 280 document.querySelectorAll('.pat-playing').forEach((e) => e.classList.remove('pat-playing')); … … 283 283 if (el) el.classList.add('pat-playing'); 284 284 } 285 // Na een htmx-navigatie is de post-DOM vervangen → highlight opnieuw zetten.285 // After an htmx navigation the post DOM is replaced → reapply the highlight. 286 286 document.body.addEventListener('htmx:afterSettle', () => { 287 287 const t = queue[currentIndex]; … … 341 341 // (@media max-width:719.98px). On wider/desktop widths the sheet is a centered 342 342 // panel, so we do NOT auto-open it there. 343 // Drie weergaves (Robin 2026-06-15): telefoon(<768) = fullscreen sheet;344 // tablet/ auto (768–1199) = grote landscape full-player (tablet + tablet-in-de-345 // auto); desktop (≥1200) = alleen mini-speler, GEEN full player. matchMedia346 // zodat dit exact de CSS-breekpunten volgt.343 // Three layouts (Robin 2026-06-15): phone (<768) = fullscreen sheet; 344 // tablet/car (768–1199) = large landscape full-player (tablet + car-mode); 345 // desktop (≥1200) = mini-player only, NO full player. matchMedia so this 346 // exactly follows the CSS breakpoints. 347 347 function playerTier() { 348 348 if (window.matchMedia('(min-width: 1200px)').matches) return 'desktop'; … … 359 359 loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true); 360 360 // Mobile: a track press from an album/playlist auto-opens the full 361 // now-playing sheet (Spotify-style) i .p.v. alleen de dunnemini-strip.362 // Alleen hier (setQueue = een verse, door-de-gebruiker-gestartequeue) —363 // n iet bij next/prev of de site-pre-seed — zodat een sheet die de gebruiker364 // bewust sloot niet vanzelf terugkomt.361 // now-playing sheet (Spotify-style) instead of just the thin mini-strip. 362 // Only here (setQueue = a fresh, user-initiated queue) — 363 // not on next/prev or the site pre-seed — so a sheet the user 364 // deliberately closed doesn't reappear by itself. 365 365 if (hasFullPlayer()) openSheet(); 366 366 } … … 377 377 p.catch((err) => { 378 378 console.warn('[pcms-audio] play() rejected:', err.name, err.message); 379 // Browser -autoplay-policy heeft 't gestopt (typisch na3-4380 // auto matische plays op iOS Safari, of als tab tijdelijk inactive381 // was). Visuele hint dat user op play moet tappen.379 // Browser autoplay policy blocked it (typically after 3-4 380 // auto-plays on iOS Safari, or when the tab was temporarily inactive). 381 // Show a visual hint for the user to tap play. 382 382 if (err && err.name === 'NotAllowedError') { 383 383 root.classList.add('audio-needs-tap'); … … 440 440 441 441 // ============================================================ 442 // 4b. Mutual exclusion — gedeelde media-registry (zie embed-player.js).443 // ============================================================ 444 // All e spelers (deze site-speler + de YouTube/SoundCloud/Spotify-embeds)445 // regist reren zich in window.pcmsMediaRegistry. Start er één, dan pauzeert de446 // vorige. Dit is de precieze vervanger van de oude focus/blur-heuristiek voor447 // de embeds met een echte JS-API. (De blur-fallback hieronder blijft staan448 // voor iframe-only embeds zonderAPI: Bandcamp/Apple Music/Vimeo.)442 // 4b. Mutual exclusion — shared media registry (see embed-player.js). 443 // ============================================================ 444 // All players (this site player + YouTube/SoundCloud/Spotify embeds) 445 // register themselves in window.pcmsMediaRegistry. Starting one pauses 446 // the previous. This is the precise replacement for the old focus/blur 447 // heuristic for embeds with a real JS API. (The blur fallback below stays 448 // for iframe-only embeds without an API: Bandcamp/Apple Music/Vimeo.) 449 449 function mediaRegistry() { 450 450 if (window.pcmsMediaRegistry) return window.pcmsMediaRegistry; … … 467 467 // 5. Audio element events → UI sync 468 468 // ============================================================ 469 // Error -counter voorkomt infinite-loop als ALLE tracks broken zijn.469 // Error counter prevents an infinite loop when ALL tracks are broken. 470 470 let consecutiveErrors = 0; 471 471 … … 473 473 isPlaying = true; 474 474 root.classList.add('is-playing'); 475 root.classList.remove('audio-needs-tap'); // verstop tap-hint 476 mediaRegistry().setActive(registrySelf); // pauzeer eventueel spelende embeds 477 }); 478 // Reset de error-teller pas bij ECHTE playback-start (`playing`), niet bij 479 // het eager `play`-event. `play` vuurt vóór een eventuele netwerk-/decode- 480 // fout, dus resetten daar zou de 3-strikes-stop nooit laten triggeren bij 481 // een kapotte track → infinite "next"-loop. `playing` vuurt alleen als er 482 // daadwerkelijk audio speelt. 475 root.classList.remove('audio-needs-tap'); // hide tap hint 476 mediaRegistry().setActive(registrySelf); // pause any currently playing embeds 477 }); 478 // Reset the error counter only on a REAL playback start (`playing`), not the 479 // eager `play` event. `play` fires before any network/decode error, so resetting 480 // there would prevent the 3-strikes stop from ever triggering on a broken 481 // track → infinite "next" loop. `playing` only fires when audio is actually playing. 483 482 audio.addEventListener('playing', () => { consecutiveErrors = 0; preloadNext(); }); 484 483 audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); }); … … 488 487 console.error('[pcms-audio] playback error', code, audio.src, e); 489 488 consecutiveErrors++; 490 // Bij netwerk/decode-fout: skip naar volgende track ipv stilstaan.491 // Max 3 fouten op rij voordat we opgeven (andersinfinite loop).489 // On network/decode error: skip to next track instead of stalling. 490 // Max 3 consecutive errors before giving up (otherwise infinite loop). 492 491 if (consecutiveErrors < 3 && queue.length > 1) { 493 console.warn('[pcms-audio] auto-skip naar volgende naerror', consecutiveErrors);492 console.warn('[pcms-audio] auto-skip to next after error', consecutiveErrors); 494 493 setTimeout(next, 400); 495 494 } … … 545 544 // 7. Sheet expand/close + drag-down-to-close 546 545 // ============================================================ 547 // Back -knop sluit de sheet op mobiel: bij openen pushen we een history-entry,548 // zodat de telefoon-terugknop (popstate) eerst de sheet sluit i.p.v. de pagina549 // te verlaten. We balanceren 'm bij een UI-sluitingvia history.back().546 // Back button closes the sheet on mobile: on open we push a history entry 547 // so the phone back button (popstate) closes the sheet first instead of 548 // leaving the page. We balance it on a UI-initiated close via history.back(). 550 549 let sheetHistoryPushed = false; 551 550 552 551 function openSheet() { 553 if (!hasFullPlayer()) return; // desktop (≥1200): geen full player, alleen mini-speler552 if (!hasFullPlayer()) return; // desktop (≥1200): no full player, mini-player only 554 553 if (sheet.classList.contains('is-open')) return; 555 554 sheet.classList.add('is-open'); 556 555 sheet.setAttribute('aria-hidden', 'false'); 557 556 document.body.classList.add('audio-sheet-locked'); 558 // Telefoon + tablet: history-entry zodat de terug-knop eerst de sheet sluit.557 // Phone + tablet: push a history entry so the back button closes the sheet first. 559 558 try { history.pushState({ pcmsSheet: true }, ''); sheetHistoryPushed = true; } catch (e) {} 560 559 } … … 566 565 sheetPanel.style.removeProperty('--pcms-drag-y'); 567 566 sheetBackdrop.style.removeProperty('--pcms-sheet-progress'); 568 // UI -sluiting (X / swipe / backdrop / Esc): pop onze eigen history-entry zodat569 // de volgende terug-knop weer normaal navigeert. Bij een popstate-sluiting570 // (de terug-knop zelf) is de entry al gepopt.567 // UI close (X / swipe / backdrop / Esc): pop our own history entry so the 568 // next back button navigates normally. On a popstate close (back button itself) 569 // the entry is already popped. 571 570 const wasPushed = sheetHistoryPushed; 572 571 sheetHistoryPushed = false; … … 576 575 if (sheet.classList.contains('is-open')) closeSheet(true); 577 576 }); 578 // Mini-player-info aanklikken:579 // - DESKTOP (≥ 768px): spring naar de post waar de track vandaan komt (als bekend),580 // via htmx zodat de audio blijft spelen. Geen post bekend → val terug op de sheet.581 // - MOBI EL: altijd de uitgebreide speler-sheet openen (huidig gedrag).577 // Clicking the mini-player track info: 578 // - DESKTOP (≥1200px): jump to the post the track came from (if known), 579 // via htmx so audio keeps playing. No post known → fall back to the sheet. 580 // - MOBILE/TABLET: always open the full now-playing sheet. 582 581 function scrollToTrack(trackId) { 583 582 if (!trackId) { window.scrollTo(0, 0); return; } … … 594 593 const p = window.htmx.ajax('GET', url, { target: '#pcms-main', swap: 'innerHTML' }); 595 594 history.pushState({}, '', url + hash); 596 // Na de swap naar de track scrollen (klein uitstel zodat de globale597 // afterSwap -top-scroll eerst is geweest); val terug op top als niet gevonden.595 // Scroll to the track after the swap (small delay so the global 596 // afterSwap scroll-to-top runs first); fall back to top if not found. 598 597 const go = () => setTimeout(() => scrollToTrack(trackId), 60); 599 598 if (p && typeof p.then === 'function') p.then(go); else setTimeout(go, 150); 600 599 return; 601 } catch (e) { /* val terug op volledige navigatie*/ }600 } catch (e) { /* fall back to full navigation */ } 602 601 } 603 602 location.href = url + hash; … … 605 604 expandTrigger.addEventListener('click', () => { 606 605 const t = queue[currentIndex]; 607 // Alleen op échte desktop (≥1200, geen full-player) springen we naar de post.608 // Tablet + telefoon hebben een full-player → open die (zoals mobiel).606 // Only on true desktop (≥1200, no full-player) do we jump to the post. 607 // Tablet + phone have a full-player → open it (same as mobile behaviour). 609 608 const jumpToPost = !hasFullPlayer(); 610 609 if (jumpToPost && t) { 611 // 1) Track speelde vanuit een post → die URL kennen we al.610 // 1) Track played from a post → we already know that URL. 612 611 if (t.postUrl) { goToPost(t.postUrl, t.id); return; } 613 // 2) Site- brede track (geen postUrl) → zoek de post op via de track-id.612 // 2) Site-wide track (no postUrl) → look up the post via the track id. 614 613 if (t.id) { 615 614 fetch('/audio/track/' + encodeURIComponent(t.id) + '/post') … … 627 626 if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet(); 628 627 }); 629 // Naar desktop-breedte (≥1200) gesleept terwijl de full player open is? Sluiten—630 // op desktop bestaat de full player niet.628 // Resized to desktop width (≥1200) while the full player is open? Close it — 629 // the full player doesn't exist on desktop. 631 630 window.addEventListener('resize', () => { 632 631 if (!hasFullPlayer() && sheet.classList.contains('is-open')) closeSheet(); 633 632 }); 634 633 635 // Vangnet voor mutual exclusion. Voor YouTube/SoundCloud/Spotify-embeds doet de636 // registry dit al precies (echte play-events). Maar voor iframe-only embeds637 // ZONDER JS-API (Bandcamp/Apple/Vimeo) én voor de iframe-FALLBACK (als een638 // ad-blocker de player-API blokkeert) is er geen play-event: daar vangen we het639 // af via focus. Klikt de gebruiker zo'n iframe aan → window 'blur' → pauzeer640 // onze speler. (Voor de API-embeds is dit hooguit een onschadelijke dubbele641 // pauze.)634 // Fallback for mutual exclusion. For YouTube/SoundCloud/Spotify embeds the 635 // registry already handles this precisely (real play events). But for 636 // iframe-only embeds WITHOUT a JS API (Bandcamp/Apple/Vimeo) and for the 637 // iframe FALLBACK (when an ad-blocker blocks the player API) there is no 638 // play event: we catch those via focus. User clicks such an iframe → 639 // window 'blur' → pause our player. (For API embeds this is at worst a 640 // harmless double-pause.) 642 641 window.addEventListener('blur', () => { 643 642 setTimeout(() => { 644 643 const el = document.activeElement; 645 // Alleen embed-iframes (binnen .folio-embed) pauzeren de speler — niet een646 // willekeurig iframe (captcha/reclame/kaart) dat per ongeluk focus krijgt.644 // Only embed iframes (inside .folio-embed) pause the player — not a 645 // random iframe (captcha/ad/map) that happens to receive focus. 647 646 if (el && el.tagName === 'IFRAME' && el.closest('.folio-embed') && audio.src && !audio.paused) { 648 647 pause(); … … 710 709 // wrapper. For the other three the data is on the button itself. The 711 710 // handler reads from button-first, falls back to wrapper. 712 // Event -delegation op document.body i.p.v. per-knop listeners. Dit overleeft713 // HTMX history-restores: de mobiele terug-knop (popstate) laat HTMX #pcms-main714 // terugzetten uit z'n snapshot; een per-element `data-pcms-attached`-vlag zou715 // dan dode knoppen geven (vlag ingebakken in de snapshot, listener weg). Eén716 // gedelegeerde listener werkt ongeacht hoe vaak de DOM ge(her)swapt wordt.711 // Event delegation on document.body instead of per-button listeners. This 712 // survives HTMX history-restores: the mobile back button (popstate) lets HTMX 713 // restore #pcms-main from its snapshot; a per-element `data-pcms-attached` flag 714 // would leave dead buttons (flag baked into the snapshot, listener gone). One 715 // delegated listener works regardless of how many times the DOM is (re)swapped. 717 716 const PLAY_SELECTOR = 718 717 '.post-audio-track .pat-play, .post-album-tracks .pat-row, .post-album-cover-btn, .post-album-playall'; … … 722 721 e.preventDefault(); 723 722 e.stopPropagation(); 724 // De post waar je VANAF afspeelt = de pagina waar je nu bent (de embeds staan725 // in de post-content). Bewaar 'm op de track(s) zodat de mini-player er op726 // desktop naartoe kan springen — overleeft ook de sessionStorage-resume.723 // The post you're playing FROM = the current page (embeds live in post content). 724 // Store it on the track(s) so the desktop mini-player can jump back to it — 725 // survives the sessionStorage resume as well. 727 726 const postUrl = location.pathname + location.search; 728 727 // Resolve metadata: button-first, then closest .post-audio-track wrapper … … 804 803 805 804 // ============================================================ 806 // 10. Sessi e-persistentie — speler "blijft" over page-navigaties heen807 // ============================================================ 808 // Een <audio> overleeft geen volledige page-load (en cross-context navigatie809 // — bv. naar de headerloze hub-overview — is bewust full-nav). We bewaren de810 // sessie in sessionStorage en herstellen + hervatten 'm op de volgende pagina:811 // de speler staat er weer met dezelfde track op dezelfde positie. In Chrome812 // (hoge media-engagement) speelt 'ie meteen door; staat de browser autoplay813 // niet toe, dan staat 'ie klaar op die plek (één tik = verder).805 // 10. Session persistence — player "survives" across page navigations 806 // ============================================================ 807 // An <audio> element doesn't survive a full page load (and cross-context 808 // navigation — e.g. to the headerless hub overview — is intentionally a 809 // full-nav). We save the session to sessionStorage and restore + resume it 810 // on the next page: the player comes back with the same track at the same 811 // position. In Chrome (high media engagement) it resumes immediately; 812 // if the browser blocks autoplay it waits at that position (one tap = play). 814 813 const PLAYER_STATE_KEY = 'pcms-player-state'; 815 814 let pendingSeek = 0; … … 830 829 audio.addEventListener('ended', savePlayerState); 831 830 setInterval(() => { if (audio.src && !audio.paused) savePlayerState(); }, 5000); 832 // Herstelde positie toepassen zodra de track-metadata binnen is.831 // Apply the restored position once track metadata is available. 833 832 audio.addEventListener('loadedmetadata', () => { 834 833 if (pendingSeek > 0 && isFinite(audio.duration) && audio.duration > 0) { … … 845 844 pendingSeek = s.time || 0; 846 845 const idx = Math.max(0, Math.min(s.currentIndex || 0, queue.length - 1)); 847 // playing -> fetch + (poging tot) hervatten; gepauzeerd -> alleen metadata.846 // playing → fetch + (attempt to) resume; paused → meta-only. 848 847 loadTrack(idx, !!s.playing, !s.playing); 849 848 return true; … … 853 852 // 11. Site-level pre-seed (window.PCMS_SITE_TRACKS) 854 853 // ============================================================ 855 // Een actieve sessie (restore) wint van de pagina-seed, zodat lopende muziek856 // doorgaat i.p.v. vervangen te worden door de tracks van de nieuwe pagina.854 // An active session (restore) wins over the page seed, so music that is 855 // already playing continues instead of being replaced by the new page's tracks. 857 856 if (!restorePlayerState()) { 858 857 if (Array.isArray(window.PCMS_SITE_TRACKS) && window.PCMS_SITE_TRACKS.length) { … … 864 863 cover: t.cover_url || t.cover || '', 865 864 })); 866 // Queue alleen klaarzetten — de speler-balk verschijnt PAS bij de eerste867 // audio-klik (een .post-audio-track of de mini-speler-play roept setQueue/868 // loadTrack → die toont de balk). Geen pre-seed-balk meer op page-load.865 // Only prime the queue — the player bar appears only on the first audio click 866 // (.post-audio-track or the mini-player play button calls setQueue/loadTrack, 867 // which shows the bar). No more pre-seed bar on page load. 869 868 currentIndex = 0; 870 869 } -
src/assets/js/embed-player.js
rbb42dfb r834bcc3 1 1 /** 2 * Klonkt Embed Player — eigen, in-huisstijl media-embeds bovenop de ÉCHTE3 * player -API's (YouTube IFrame API, SoundCloud Widget API, Spotify iFrame API).2 * Klonkt Embed Player — custom, on-brand media embeds on top of the REAL 3 * player APIs (YouTube IFrame API, SoundCloud Widget API, Spotify iFrame API). 4 4 * 5 * De server (AudioEmbedService) rendert per embed een placeholder:5 * The server (AudioEmbedService) renders a placeholder per embed: 6 6 * <div class="folio-embed folio-embed--<provider> pcms-embed-card" 7 7 * data-embed-provider data-embed-ref data-embed-type data-embed-url></div> 8 * Dit script bouwt daar onze eigen kaart omheen (cover/poster + onze play-knop+9 * voortgangsbalk in huisstijl) en bestuurt de onderliggende speler via de10 * platform -API, zodat play/pause/voortgang in ÓNZE handen liggen.8 * This script wraps it in our own card (cover/poster + our play button + 9 * progress bar in brand style) and controls the underlying player via the 10 * platform API, so play/pause/progress are in OUR hands. 11 11 * 12 * Capability -eerlijkheid:13 * - youtube/soundcloud → volledig eigen controls (native chrome verborgen).14 * - spotify → besturing + onze frame eromheen; Spotify's eigen UI15 * blijft binnenin (kan niet anders zonderPremium+OAuth).12 * Capability honesty: 13 * - youtube/soundcloud → fully custom controls (native chrome hidden). 14 * - spotify → controls + our frame around it; Spotify's own UI 15 * stays inside (no alternative without Premium+OAuth). 16 16 * 17 * Mutual exclusion: e lke speler (incl. de site-audiospeler in audio-player.js)18 * regist reert zich in window.pcmsMediaRegistry. Start er één → de vorige pauzeert.19 * Dit vervangt de oude focus/blur-heuristiek door echte play-events.17 * Mutual exclusion: every player (incl. the site audio player in audio-player.js) 18 * registers itself in window.pcmsMediaRegistry. Starting one pauses the previous. 19 * This replaces the old focus/blur heuristic with real play events. 20 20 * 21 * Singleton — guard tegen dubbel-init (HTMX laadt scripts soms opnieuw).21 * Singleton — guard against double-init (HTMX can reload scripts). 22 22 */ 23 23 (function () { … … 25 25 26 26 // ============================================================ 27 // 0. Gedeelde playback-registry (ook door audio-player.js gebruikt)27 // 0. Shared playback registry (also used by audio-player.js) 28 28 // ============================================================ 29 29 function registry() { … … 31 31 const r = { 32 32 _active: null, 33 // Mark eer `player` als de enige actieve; pauzeer de vorige.33 // Mark `player` as the sole active one; pause the previous. 34 34 setActive(player) { 35 35 if (this._active && this._active !== player && this._active.pause) { … … 45 45 46 46 // ============================================================ 47 // 1. Lazy script -loaders — 1 promise per platform, gedeeld overN embeds.48 // We laden een platform-script PAS als er een embed van dat platform op de49 // pagina daadwerkelijk wordt gestart.47 // 1. Lazy script loaders — 1 promise per platform, shared across N embeds. 48 // A platform script is only loaded when an embed from that platform 49 // is actually started on the page. 50 50 // ============================================================ 51 51 const scripts = {}; … … 55 55 s.src = src; s.async = true; 56 56 s.onload = () => resolve(); 57 s.onerror = () => reject(new Error('embed -script faalde: ' + src));57 s.onerror = () => reject(new Error('embed script failed: ' + src)); 58 58 document.head.appendChild(s); 59 59 }); 60 60 } 61 // Belangrijk: alle loaders REJECTEN bij een script-fout (bv. een ad-blocker die62 // het API-script blokkeert) én na een time-out — zodat de aanroeper netjes kan63 // t erugvallen op het kale platform-iframe i.p.v. eeuwig te hangen.61 // Important: all loaders REJECT on script errors (e.g. an ad-blocker blocking 62 // the API script) and on timeout — so the caller can gracefully fall back to 63 // the plain platform iframe instead of hanging indefinitely. 64 64 const API_TIMEOUT = 8000; 65 65 66 // YouTube: global e callback onYouTubeIframeAPIReady (1×) → in promise wikkelen.66 // YouTube: global callback onYouTubeIframeAPIReady (once) → wrap in a promise. 67 67 function ytApi() { 68 68 if (scripts.yt) return scripts.yt; … … 79 79 return scripts.yt; 80 80 } 81 // SoundCloud: geen globale ready-callback; resolve op script-onload, daarna82 // wacht elke widget op z'n eigen SC.Widget.Events.READY.81 // SoundCloud: no global ready callback; resolve on script onload, then 82 // each widget waits for its own SC.Widget.Events.READY. 83 83 function scApi() { 84 84 if (scripts.sc) return scripts.sc; … … 91 91 return scripts.sc; 92 92 } 93 // Spotify: global e callback onSpotifyIframeApiReady(IFrameAPI) (1×) → wrappen.93 // Spotify: global callback onSpotifyIframeApiReady(IFrameAPI) (once) → wrap in a promise. 94 94 function spotifyApi() { 95 95 if (scripts.sp) return scripts.sp; … … 101 101 }; 102 102 loadScript('https://open.spotify.com/embed/iframe-api/v1').catch(reject); 103 // Korter dan API_TIMEOUT: Spotify's bundle initialiseert snel óf helemaal104 // niet (CDN 503 / origin-gating). Niet 8s wachten vóór de iframe-fallback.103 // Shorter than API_TIMEOUT: Spotify's bundle initialises quickly or not at all 104 // (CDN 503 / origin-gating). No need to wait 8 s before the iframe fallback. 105 105 setTimeout(() => reject(new Error('Spotify API timeout')), 4000); 106 106 }); … … 108 108 } 109 109 110 // Kaal platform-iframe als fallback wanneer de JS-API geblokkeerd/onbereikbaar111 // is. Ad-blockers laten de embed-iframes doorgaans wél door. Autoplay-param112 // omdat we hier altijd in een user-gesture-context zitten (klik op play).110 // Plain platform iframe as fallback when the JS API is blocked/unreachable. 111 // Ad-blockers generally let embed iframes through. Autoplay parameter 112 // is safe here because we're always in a user-gesture context (play click). 113 113 function fallbackIframe(provider, ref, url) { 114 114 if (provider === 'youtube') { … … 138 138 const m = (url || '').match(/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{11})/); 139 139 if (m) return m[1]; 140 return null; // geen blinde slice — een ongeldige ref geeft liever niets dan een kapotteid141 } 142 // Alleen http(s) als href toelaten (defense-in-depth tegen javascript:/data:).140 return null; // no blind slice — an invalid ref returns nothing rather than a broken id 141 } 142 // Only allow http(s) as href (defense-in-depth against javascript:/data: URIs). 143 143 function safeHref(u) { 144 144 try { const p = new URL(u, location.href); return (p.protocol === 'http:' || p.protocol === 'https:') ? u : '#'; } … … 154 154 155 155 // ============================================================ 156 // 3. Card controller — b ouwt de huisstijl-kaart + delegeert naar een adapter156 // 3. Card controller — builds the on-brand card + delegates to an adapter 157 157 // ============================================================ 158 158 function buildCard(el) { … … 165 165 el.classList.remove('pcms-embed-loading'); 166 166 167 // O nze speler-zijde (registry-peer). pause() wijst naar de adapter zodra die168 // gemount is; ervoor is 't eenno-op.167 // Our player side (registry peer). pause() points to the adapter once 168 // mounted; before that it's a no-op. 169 169 let adapter = null; 170 170 const self = { pause() { if (adapter && adapter.pause) { try { adapter.pause(); } catch (e) {} } } }; 171 171 172 // Teardown -hook: aangeroepen door de MutationObserver als deze kaart uit de173 // DOM verdwijnt (HTMX-swap) → adapter opruimen (timers/iframes) + registry174 // vrijgeven, zodat er geen poll-timers of spelers blijven lekken.172 // Teardown hook: called by the MutationObserver when this card leaves the 173 // DOM (HTMX swap) → clean up the adapter (timers/iframes) + release the registry, 174 // so no poll timers or players keep leaking. 175 175 el._pcmsDestroy = function () { 176 176 try { if (adapter && adapter.destroy) adapter.destroy(); } catch (e) {} … … 182 182 let dur = 0; 183 183 184 // --- UI ophangen (verschilt per provider-type) ---184 // --- Mount the UI (differs per provider type) --- 185 185 const isVideo = provider === 'youtube'; 186 186 const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls … … 220 220 const volEl = el.querySelector('.pcms-embed-vol'); 221 221 const muteBtn = el.querySelector('.pcms-embed-mute'); 222 let vol = 100; // huidige volume 0-100 (wordt op de adapter toegepast)222 let vol = 100; // current volume 0-100 (applied to the adapter) 223 223 let preMuteVol = 100; 224 224 … … 251 251 }; 252 252 253 // API geblokkeerd/onbereikbaar → kaal platform-iframe (graceful degradation).254 // De mutual-exclusion loopt voor deze fallback via de blur-heuristiek255 // (audio-player.js), want de kaart krijgt geen.is-mounted.253 // API blocked/unreachable → plain platform iframe (graceful degradation). 254 // Mutual exclusion for this fallback runs via the blur heuristic 255 // (audio-player.js), because the card never gets .is-mounted. 256 256 function renderFallback() { 257 257 const fb = fallbackIframe(provider, ref, url); … … 271 271 el.appendChild(iframe); 272 272 273 // Mutual exclusion óók voor de fallback-iframe. Eencross-origin iframe274 // kunnen we niet via een API pauzeren, dus 'pauze' = herladen ZONDER275 // autoplay (= stop t het geluid, speler blijft zichtbaar/herstartbaar).276 // We regist reren 'm als actief: nu (= gebruiker start de embed) pauzeert de277 // site -speler/andere embeds; en als de site-speler later start, pauzeert de278 // registry dezefallback.273 // Mutual exclusion also for the fallback iframe. A cross-origin iframe 274 // cannot be paused via an API, so 'pause' = reload WITHOUT 275 // autoplay (= stops the audio, player stays visible/restartable). 276 // We register it as active: now (= user starts the embed) the 277 // site player/other embeds pause; and when the site player starts later, 278 // the registry pauses this fallback. 279 279 self.pause = function () { 280 280 try { … … 294 294 } 295 295 296 // Eerste interactie → adapter mounten + spelen. Daarna toggelt de knop.296 // First interaction → mount the adapter + play. The button toggles after that. 297 297 async function ensureMountedAndPlay() { 298 298 if (mounted) { if (adapter) adapter.play(); return; } 299 299 mounted = true; 300 // Spotify: hun speler is toch niet te skinnen (besturing-only) én de301 // iFrame -API-bundle initialiseert in de praktijk vaak niet(CDN-gating/503)302 // → niet op de API wachten, meteen het kale Spotify-iframe tonen. Instant.300 // Spotify: their player cannot be skinned (controls-only) and the 301 // iFrame API bundle often fails to initialise in practice (CDN-gating/503) 302 // → don't wait for the API, show the plain Spotify iframe immediately. 303 303 if (provider === 'spotify') { renderFallback(); return; } 304 304 el.classList.add('pcms-embed-busy'); … … 306 306 adapter = await MOUNTERS[provider](mountEl, { ref, url }, hooks); 307 307 if (el._pcmsApplyVol) el._pcmsApplyVol(); // onthouden volume toepassen 308 // is-mounted: CSS verbergt de poster (video) of de Spotify-facade en309 // toont de echte speler. Voor SoundCloud blijft onze kaart+balk staan en310 // blijft het (functionele) iframe off-screen verborgen.308 // is-mounted: CSS hides the poster (video) or Spotify facade and 309 // shows the real player. For SoundCloud our card+bar stays visible and 310 // the (functional) iframe remains hidden off-screen. 311 311 el.classList.add('is-mounted'); 312 312 adapter.play(); 313 313 } catch (err) { 314 console.warn('[pcms-embed] API niet beschikbaar, val terug op kaaliframe', provider, err);314 console.warn('[pcms-embed] API unavailable, falling back to plain iframe', provider, err); 315 315 renderFallback(); 316 316 } … … 328 328 adapter.seek(ratio * dur); 329 329 }); 330 // Volume: s chuif zet vol (0-100) en past 'm toe op de adapter (YT/SC hebben331 // setVolume). De waarde wordt onthouden en na het mounten opnieuw toegepast.330 // Volume: slider sets vol (0-100) and applies it to the adapter (YT/SC support 331 // setVolume). The value is remembered and reapplied after mounting. 332 332 function applyVol() { 333 333 if (adapter && adapter.setVolume) { try { adapter.setVolume(vol); } catch (e) {} } … … 341 341 applyVol(); 342 342 }); 343 el._pcmsApplyVol = applyVol; // door ensureMountedAndPlay aangeroepen namount343 el._pcmsApplyVol = applyVol; // called by ensureMountedAndPlay after mount 344 344 } 345 345 … … 349 349 350 350 // ============================================================ 351 // 4. Adapters — normalise ren de 3 zeer verschillende API's naar 1 vorm.352 // E lke mounter geeft { play, pause, seek } terug en roept hooks aan met353 // second en (units worden hier rechtgetrokken).351 // 4. Adapters — normalise the 3 very different APIs to one common shape. 352 // Each mounter returns { play, pause, seek } and calls hooks with 353 // seconds (units are normalised here). 354 354 // ============================================================ 355 355 const MOUNTERS = { … … 403 403 }, 404 404 405 // ---- SoundCloud: Widget API, visual=false, eigencontrols ----405 // ---- SoundCloud: Widget API, visual=false, custom controls ---- 406 406 async soundcloud(mountEl, { ref, url }, hooks) { 407 407 const SC = await scApi(); 408 // Iframe zelf bouwen (kale balk) en daarna SC.Widget eraan hangen.408 // Build the iframe ourselves (bare bar) and then attach SC.Widget to it. 409 409 const iframe = document.createElement('iframe'); 410 410 iframe.allow = 'autoplay'; … … 453 453 }, 454 454 455 // ---- Spotify: iFrame API — besturing + onze frame; geen eigenskin ----455 // ---- Spotify: iFrame API — controls + our frame; no custom skin ---- 456 456 async spotify(mountEl, { ref, url }, hooks) { 457 457 const IFrameAPI = await spotifyApi(); … … 473 473 const d = e && e.data ? e.data : {}; 474 474 hooks.onProgress((d.position || 0) / 1000, (d.duration || 0) / 1000); 475 // Geen betrouwbare 'ended'-event bij Spotify; we behandelen elke476 // isPaused -overgang als play/pause. Einde = gewoon een pauze (de balk477 // blijft op de eindpositie i.p.v. misleidend naar 0 te springen).475 // No reliable 'ended' event from Spotify; we treat every 476 // isPaused transition as play/pause. End = just a pause (the bar 477 // stays at the end position rather than misleadingly jumping to 0). 478 478 if (d.isPaused === false && lastPaused) { lastPaused = false; hooks.onPlay(); } 479 479 else if (d.isPaused === true && !lastPaused) { lastPaused = true; hooks.onPause(); } … … 486 486 487 487 // ============================================================ 488 // 5. Scan + HTMX -/DOM-mutatie-aware (de site navigeert deels via HTMX-swaps)488 // 5. Scan + HTMX/DOM-mutation-aware (the site partially navigates via HTMX swaps) 489 489 // ============================================================ 490 490 function scan(root) { … … 492 492 if (el.dataset.embedInit) return; 493 493 el.dataset.embedInit = '1'; 494 try { buildCard(el); } catch (e) { console.error('[pcms-embed] buildCard fa alde', e); }494 try { buildCard(el); } catch (e) { console.error('[pcms-embed] buildCard failed', e); } 495 495 }); 496 496 } … … 501 501 scan(document); 502 502 } 503 // HTMX vervangt #pcms-main bij interne navigatie → opnieuw scannen.503 // HTMX replaces #pcms-main on internal navigation → rescan. 504 504 document.body.addEventListener('htmx:afterSwap', (e) => scan(e.target || document)); 505 505 document.body.addEventListener('htmx:load', (e) => scan(e.target || document)); 506 506 507 // Teardown bij verwijdering uit de DOM (HTMX vervangt #pcms-main innerHTML, of 508 // een SPA-achtige swap). Zonder dit blijven YouTube-poll-timers + adapters/ 509 // iframes hangen als je wegnavigeert terwijl een embed speelt → CPU/geheugenlek 510 // dat per navigatie opstapelt. We roepen el._pcmsDestroy() aan voor elke kaart 511 // die echt uit het document verdwijnt. 507 // Teardown on DOM removal (HTMX replaces #pcms-main innerHTML, or an SPA-like 508 // swap). Without this, YouTube poll timers + adapters/iframes linger when 509 // navigating away while an embed is playing → CPU/memory leak that accumulates 510 // per navigation. We call el._pcmsDestroy() for every card that truly leaves the document. 512 511 const teardownObserver = new MutationObserver((muts) => { 513 512 for (const m of muts) { -
src/assets/js/smooth-scroll.js
rbb42dfb r834bcc3 2 2 (function () { 3 3 if (typeof Lenis === "undefined") return; 4 // Skip o p touch/mobile — native momentum is beter danJS lib4 // Skip on touch/mobile — native momentum scroll is better than a JS lib 5 5 var isMobile = window.matchMedia("(pointer: coarse)").matches || window.innerWidth < 1024; 6 6 if (isMobile) return;
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)