Changeset 834bcc3 in Klonkt for src/assets/js


Ignore:
Timestamp:
06/23/2026 06:14:27 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
d774679
Parents:
bb42dfb
Message:

i18n: translate Dutch code comments to English across src/

Comments in routes/services/views/config/middleware/assets translated to
English for the public repo. A few dev-facing throw/console message strings
were Englished too. No user-facing UI strings or i18n dictionary values changed
(src/services/i18n.js untouched). Logic unchanged.

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

Location:
src/assets/js
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/audio-player.js

    rbb42dfb r834bcc3  
    230230    }
    231231    currentObjectUrl = objUrl;
    232     // Schone overgang: pause + load forceert reset van internal state na
    233     // meerdere 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).
    234234    try { audio.pause(); } catch (e) {}
    235235    audio.src = objUrl;
     
    276276  // Used by the site pre-seed so opening a page doesn't auto-download audio;
    277277  // 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).
    279279  function markPlaying(trackId) {
    280280    document.querySelectorAll('.pat-playing').forEach((e) => e.classList.remove('pat-playing'));
     
    283283    if (el) el.classList.add('pat-playing');
    284284  }
    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.
    286286  document.body.addEventListener('htmx:afterSettle', () => {
    287287    const t = queue[currentIndex];
     
    341341  // (@media max-width:719.98px). On wider/desktop widths the sheet is a centered
    342342  // 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. matchMedia
    346   // 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.
    347347  function playerTier() {
    348348    if (window.matchMedia('(min-width: 1200px)').matches) return 'desktop';
     
    359359    loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true);
    360360    // Mobile: a track press from an album/playlist auto-opens the full
    361     // now-playing sheet (Spotify-style) i.p.v. alleen de dunne mini-strip.
    362     // Alleen hier (setQueue = een verse, door-de-gebruiker-gestarte queue) —
    363     // niet bij next/prev of de site-pre-seed — zodat een sheet die de gebruiker
    364     // 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.
    365365    if (hasFullPlayer()) openSheet();
    366366  }
     
    377377      p.catch((err) => {
    378378        console.warn('[pcms-audio] play() rejected:', err.name, err.message);
    379         // Browser-autoplay-policy heeft 't gestopt (typisch na 3-4
    380         // automatische plays op iOS Safari, of als tab tijdelijk inactive
    381         // 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.
    382382        if (err && err.name === 'NotAllowedError') {
    383383          root.classList.add('audio-needs-tap');
     
    440440
    441441  // ============================================================
    442   // 4b. Mutual exclusion — gedeelde media-registry (zie embed-player.js).
    443   // ============================================================
    444   // Alle spelers (deze site-speler + de YouTube/SoundCloud/Spotify-embeds)
    445   // registreren zich in window.pcmsMediaRegistry. Start er één, dan pauzeert de
    446   // vorige. Dit is de precieze vervanger van de oude focus/blur-heuristiek voor
    447   // de embeds met een echte JS-API. (De blur-fallback hieronder blijft staan
    448   // voor iframe-only embeds zonder API: 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.)
    449449  function mediaRegistry() {
    450450    if (window.pcmsMediaRegistry) return window.pcmsMediaRegistry;
     
    467467  // 5. Audio element events → UI sync
    468468  // ============================================================
    469   // Error-counter voorkomt infinite-loop als ALLE tracks broken zijn.
     469  // Error counter prevents an infinite loop when ALL tracks are broken.
    470470  let consecutiveErrors = 0;
    471471
     
    473473    isPlaying = true;
    474474    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.
    483482  audio.addEventListener('playing', () => { consecutiveErrors = 0; preloadNext(); });
    484483  audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); });
     
    488487    console.error('[pcms-audio] playback error', code, audio.src, e);
    489488    consecutiveErrors++;
    490     // Bij netwerk/decode-fout: skip naar volgende track ipv stilstaan.
    491     // Max 3 fouten op rij voordat we opgeven (anders infinite loop).
     489    // On network/decode error: skip to next track instead of stalling.
     490    // Max 3 consecutive errors before giving up (otherwise infinite loop).
    492491    if (consecutiveErrors < 3 && queue.length > 1) {
    493       console.warn('[pcms-audio] auto-skip naar volgende na error', consecutiveErrors);
     492      console.warn('[pcms-audio] auto-skip to next after error', consecutiveErrors);
    494493      setTimeout(next, 400);
    495494    }
     
    545544  // 7. Sheet expand/close + drag-down-to-close
    546545  // ============================================================
    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 pagina
    549   // te verlaten. We balanceren 'm bij een UI-sluiting via 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().
    550549  let sheetHistoryPushed = false;
    551550
    552551  function openSheet() {
    553     if (!hasFullPlayer()) return; // desktop (≥1200): geen full player, alleen mini-speler
     552    if (!hasFullPlayer()) return; // desktop (≥1200): no full player, mini-player only
    554553    if (sheet.classList.contains('is-open')) return;
    555554    sheet.classList.add('is-open');
    556555    sheet.setAttribute('aria-hidden', 'false');
    557556    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.
    559558    try { history.pushState({ pcmsSheet: true }, ''); sheetHistoryPushed = true; } catch (e) {}
    560559  }
     
    566565    sheetPanel.style.removeProperty('--pcms-drag-y');
    567566    sheetBackdrop.style.removeProperty('--pcms-sheet-progress');
    568     // UI-sluiting (X / swipe / backdrop / Esc): pop onze eigen history-entry zodat
    569     // de volgende terug-knop weer normaal navigeert. Bij een popstate-sluiting
    570     // (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.
    571570    const wasPushed = sheetHistoryPushed;
    572571    sheetHistoryPushed = false;
     
    576575    if (sheet.classList.contains('is-open')) closeSheet(true);
    577576  });
    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   //  - MOBIEL: 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.
    582581  function scrollToTrack(trackId) {
    583582    if (!trackId) { window.scrollTo(0, 0); return; }
     
    594593        const p = window.htmx.ajax('GET', url, { target: '#pcms-main', swap: 'innerHTML' });
    595594        history.pushState({}, '', url + hash);
    596         // Na de swap naar de track scrollen (klein uitstel zodat de globale
    597         // 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.
    598597        const go = () => setTimeout(() => scrollToTrack(trackId), 60);
    599598        if (p && typeof p.then === 'function') p.then(go); else setTimeout(go, 150);
    600599        return;
    601       } catch (e) { /* val terug op volledige navigatie */ }
     600      } catch (e) { /* fall back to full navigation */ }
    602601    }
    603602    location.href = url + hash;
     
    605604  expandTrigger.addEventListener('click', () => {
    606605    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).
    609608    const jumpToPost = !hasFullPlayer();
    610609    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.
    612611      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.
    614613      if (t.id) {
    615614        fetch('/audio/track/' + encodeURIComponent(t.id) + '/post')
     
    627626    if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet();
    628627  });
    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.
    631630  window.addEventListener('resize', () => {
    632631    if (!hasFullPlayer() && sheet.classList.contains('is-open')) closeSheet();
    633632  });
    634633
    635   // Vangnet voor mutual exclusion. Voor YouTube/SoundCloud/Spotify-embeds doet de
    636   // registry dit al precies (echte play-events). Maar voor iframe-only embeds
    637   // ZONDER JS-API (Bandcamp/Apple/Vimeo) én voor de iframe-FALLBACK (als een
    638   // ad-blocker de player-API blokkeert) is er geen play-event: daar vangen we het
    639   // af via focus. Klikt de gebruiker zo'n iframe aan → window 'blur' → pauzeer
    640   // onze speler. (Voor de API-embeds is dit hooguit een onschadelijke dubbele
    641   // 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.)
    642641  window.addEventListener('blur', () => {
    643642    setTimeout(() => {
    644643      const el = document.activeElement;
    645       // Alleen embed-iframes (binnen .folio-embed) pauzeren de speler — niet een
    646       // 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.
    647646      if (el && el.tagName === 'IFRAME' && el.closest('.folio-embed') && audio.src && !audio.paused) {
    648647        pause();
     
    710709  // wrapper. For the other three the data is on the button itself. The
    711710  // handler reads from button-first, falls back to wrapper.
    712   // Event-delegation op document.body i.p.v. per-knop listeners. Dit overleeft
    713   // HTMX history-restores: de mobiele terug-knop (popstate) laat HTMX #pcms-main
    714   // terugzetten uit z'n snapshot; een per-element `data-pcms-attached`-vlag zou
    715   // dan dode knoppen geven (vlag ingebakken in de snapshot, listener weg). Eén
    716   // 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.
    717716  const PLAY_SELECTOR =
    718717    '.post-audio-track .pat-play, .post-album-tracks .pat-row, .post-album-cover-btn, .post-album-playall';
     
    722721    e.preventDefault();
    723722    e.stopPropagation();
    724     // De post waar je VANAF afspeelt = de pagina waar je nu bent (de embeds staan
    725     // in de post-content). Bewaar 'm op de track(s) zodat de mini-player er op
    726     // 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.
    727726    const postUrl = location.pathname + location.search;
    728727    // Resolve metadata: button-first, then closest .post-audio-track wrapper
     
    804803
    805804  // ============================================================
    806   // 10. Sessie-persistentie — speler "blijft" over page-navigaties heen
    807   // ============================================================
    808   // Een <audio> overleeft geen volledige page-load (en cross-context navigatie
    809   // — bv. naar de headerloze hub-overview — is bewust full-nav). We bewaren de
    810   // sessie in sessionStorage en herstellen + hervatten 'm op de volgende pagina:
    811   // de speler staat er weer met dezelfde track op dezelfde positie. In Chrome
    812   // (hoge media-engagement) speelt 'ie meteen door; staat de browser autoplay
    813   // 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).
    814813  const PLAYER_STATE_KEY = 'pcms-player-state';
    815814  let pendingSeek = 0;
     
    830829  audio.addEventListener('ended', savePlayerState);
    831830  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.
    833832  audio.addEventListener('loadedmetadata', () => {
    834833    if (pendingSeek > 0 && isFinite(audio.duration) && audio.duration > 0) {
     
    845844    pendingSeek = s.time || 0;
    846845    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.
    848847    loadTrack(idx, !!s.playing, !s.playing);
    849848    return true;
     
    853852  // 11. Site-level pre-seed (window.PCMS_SITE_TRACKS)
    854853  // ============================================================
    855   // Een actieve sessie (restore) wint van de pagina-seed, zodat lopende muziek
    856   // 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.
    857856  if (!restorePlayerState()) {
    858857    if (Array.isArray(window.PCMS_SITE_TRACKS) && window.PCMS_SITE_TRACKS.length) {
     
    864863        cover:  t.cover_url || t.cover || '',
    865864      }));
    866       // Queue alleen klaarzetten — de speler-balk verschijnt PAS bij de eerste
    867       // 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.
    869868      currentIndex = 0;
    870869    }
  • src/assets/js/embed-player.js

    rbb42dfb r834bcc3  
    11/**
    2  * Klonkt Embed Player — eigen, in-huisstijl media-embeds bovenop de ÉCHTE
    3  * 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).
    44 *
    5  * De server (AudioEmbedService) rendert per embed een placeholder:
     5 * The server (AudioEmbedService) renders a placeholder per embed:
    66 *   <div class="folio-embed folio-embed--<provider> pcms-embed-card"
    77 *        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 de
    10  * 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.
    1111 *
    12  * Capability-eerlijkheid:
    13  *  - youtube/soundcloud  → volledig eigen controls (native chrome verborgen).
    14  *  - spotify             → besturing + onze frame eromheen; Spotify's eigen UI
    15  *                          blijft binnenin (kan niet anders zonder Premium+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).
    1616 *
    17  * Mutual exclusion: elke speler (incl. de site-audiospeler in audio-player.js)
    18  * registreert 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.
    2020 *
    21  * Singleton — guard tegen dubbel-init (HTMX laadt scripts soms opnieuw).
     21 * Singleton — guard against double-init (HTMX can reload scripts).
    2222 */
    2323(function () {
     
    2525
    2626  // ============================================================
    27   // 0. Gedeelde playback-registry (ook door audio-player.js gebruikt)
     27  // 0. Shared playback registry (also used by audio-player.js)
    2828  // ============================================================
    2929  function registry() {
     
    3131    const r = {
    3232      _active: null,
    33       // Markeer `player` als de enige actieve; pauzeer de vorige.
     33      // Mark `player` as the sole active one; pause the previous.
    3434      setActive(player) {
    3535        if (this._active && this._active !== player && this._active.pause) {
     
    4545
    4646  // ============================================================
    47   // 1. Lazy script-loaders — 1 promise per platform, gedeeld over N embeds.
    48   //    We laden een platform-script PAS als er een embed van dat platform op de
    49   //    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.
    5050  // ============================================================
    5151  const scripts = {};
     
    5555      s.src = src; s.async = true;
    5656      s.onload = () => resolve();
    57       s.onerror = () => reject(new Error('embed-script faalde: ' + src));
     57      s.onerror = () => reject(new Error('embed script failed: ' + src));
    5858      document.head.appendChild(s);
    5959    });
    6060  }
    61   // Belangrijk: alle loaders REJECTEN bij een script-fout (bv. een ad-blocker die
    62   // het API-script blokkeert) én na een time-out — zodat de aanroeper netjes kan
    63   // terugvallen 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.
    6464  const API_TIMEOUT = 8000;
    6565
    66   // YouTube: globale callback onYouTubeIframeAPIReady (1×) → in promise wikkelen.
     66  // YouTube: global callback onYouTubeIframeAPIReady (once) → wrap in a promise.
    6767  function ytApi() {
    6868    if (scripts.yt) return scripts.yt;
     
    7979    return scripts.yt;
    8080  }
    81   // SoundCloud: geen globale ready-callback; resolve op script-onload, daarna
    82   // 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.
    8383  function scApi() {
    8484    if (scripts.sc) return scripts.sc;
     
    9191    return scripts.sc;
    9292  }
    93   // Spotify: globale callback onSpotifyIframeApiReady(IFrameAPI) (1×) → wrappen.
     93  // Spotify: global callback onSpotifyIframeApiReady(IFrameAPI) (once) → wrap in a promise.
    9494  function spotifyApi() {
    9595    if (scripts.sp) return scripts.sp;
     
    101101      };
    102102      loadScript('https://open.spotify.com/embed/iframe-api/v1').catch(reject);
    103       // Korter dan API_TIMEOUT: Spotify's bundle initialiseert snel óf helemaal
    104       // 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.
    105105      setTimeout(() => reject(new Error('Spotify API timeout')), 4000);
    106106    });
     
    108108  }
    109109
    110   // Kaal platform-iframe als fallback wanneer de JS-API geblokkeerd/onbereikbaar
    111   // is. Ad-blockers laten de embed-iframes doorgaans wél door. Autoplay-param
    112   // 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).
    113113  function fallbackIframe(provider, ref, url) {
    114114    if (provider === 'youtube') {
     
    138138    const m = (url || '').match(/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{11})/);
    139139    if (m) return m[1];
    140     return null;  // geen blinde slice — een ongeldige ref geeft liever niets dan een kapotte id
    141   }
    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).
    143143  function safeHref(u) {
    144144    try { const p = new URL(u, location.href); return (p.protocol === 'http:' || p.protocol === 'https:') ? u : '#'; }
     
    154154
    155155  // ============================================================
    156   // 3. Card controller — bouwt de huisstijl-kaart + delegeert naar een adapter
     156  // 3. Card controller — builds the on-brand card + delegates to an adapter
    157157  // ============================================================
    158158  function buildCard(el) {
     
    165165    el.classList.remove('pcms-embed-loading');
    166166
    167     // Onze speler-zijde (registry-peer). pause() wijst naar de adapter zodra die
    168     // gemount is; ervoor is 't een no-op.
     167    // Our player side (registry peer). pause() points to the adapter once
     168    // mounted; before that it's a no-op.
    169169    let adapter = null;
    170170    const self = { pause() { if (adapter && adapter.pause) { try { adapter.pause(); } catch (e) {} } } };
    171171
    172     // Teardown-hook: aangeroepen door de MutationObserver als deze kaart uit de
    173     // DOM verdwijnt (HTMX-swap) → adapter opruimen (timers/iframes) + registry
    174     // 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.
    175175    el._pcmsDestroy = function () {
    176176      try { if (adapter && adapter.destroy) adapter.destroy(); } catch (e) {}
     
    182182    let dur = 0;
    183183
    184     // --- UI ophangen (verschilt per provider-type) ---
     184    // --- Mount the UI (differs per provider type) ---
    185185    const isVideo = provider === 'youtube';
    186186    const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls
     
    220220    const volEl = el.querySelector('.pcms-embed-vol');
    221221    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)
    223223    let preMuteVol = 100;
    224224
     
    251251    };
    252252
    253     // API geblokkeerd/onbereikbaar → kaal platform-iframe (graceful degradation).
    254     // De mutual-exclusion loopt voor deze fallback via de blur-heuristiek
    255     // (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.
    256256    function renderFallback() {
    257257      const fb = fallbackIframe(provider, ref, url);
     
    271271      el.appendChild(iframe);
    272272
    273       // Mutual exclusion óók voor de fallback-iframe. Een cross-origin iframe
    274       // kunnen we niet via een API pauzeren, dus 'pauze' = herladen ZONDER
    275       // autoplay (= stopt het geluid, speler blijft zichtbaar/herstartbaar).
    276       // We registreren 'm als actief: nu (= gebruiker start de embed) pauzeert de
    277       // site-speler/andere embeds; en als de site-speler later start, pauzeert de
    278       // registry deze fallback.
     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.
    279279      self.pause = function () {
    280280        try {
     
    294294    }
    295295
    296     // Eerste interactie → adapter mounten + spelen. Daarna toggelt de knop.
     296    // First interaction → mount the adapter + play. The button toggles after that.
    297297    async function ensureMountedAndPlay() {
    298298      if (mounted) { if (adapter) adapter.play(); return; }
    299299      mounted = true;
    300       // Spotify: hun speler is toch niet te skinnen (besturing-only) én de
    301       // 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.
    303303      if (provider === 'spotify') { renderFallback(); return; }
    304304      el.classList.add('pcms-embed-busy');
     
    306306        adapter = await MOUNTERS[provider](mountEl, { ref, url }, hooks);
    307307        if (el._pcmsApplyVol) el._pcmsApplyVol();  // onthouden volume toepassen
    308         // is-mounted: CSS verbergt de poster (video) of de Spotify-facade en
    309         // toont de echte speler. Voor SoundCloud blijft onze kaart+balk staan en
    310         // 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.
    311311        el.classList.add('is-mounted');
    312312        adapter.play();
    313313      } catch (err) {
    314         console.warn('[pcms-embed] API niet beschikbaar, val terug op kaal iframe', provider, err);
     314        console.warn('[pcms-embed] API unavailable, falling back to plain iframe', provider, err);
    315315        renderFallback();
    316316      }
     
    328328      adapter.seek(ratio * dur);
    329329    });
    330     // Volume: schuif zet vol (0-100) en past 'm toe op de adapter (YT/SC hebben
    331     // 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.
    332332    function applyVol() {
    333333      if (adapter && adapter.setVolume) { try { adapter.setVolume(vol); } catch (e) {} }
     
    341341      applyVol();
    342342    });
    343     el._pcmsApplyVol = applyVol;  // door ensureMountedAndPlay aangeroepen na mount
     343    el._pcmsApplyVol = applyVol;  // called by ensureMountedAndPlay after mount
    344344  }
    345345
     
    349349
    350350  // ============================================================
    351   // 4. Adapters — normaliseren de 3 zeer verschillende API's naar 1 vorm.
    352   //    Elke mounter geeft { play, pause, seek } terug en roept hooks aan met
    353   //    seconden (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).
    354354  // ============================================================
    355355  const MOUNTERS = {
     
    403403    },
    404404
    405     // ---- SoundCloud: Widget API, visual=false, eigen controls ----
     405    // ---- SoundCloud: Widget API, visual=false, custom controls ----
    406406    async soundcloud(mountEl, { ref, url }, hooks) {
    407407      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.
    409409      const iframe = document.createElement('iframe');
    410410      iframe.allow = 'autoplay';
     
    453453    },
    454454
    455     // ---- Spotify: iFrame API — besturing + onze frame; geen eigen skin ----
     455    // ---- Spotify: iFrame API — controls + our frame; no custom skin ----
    456456    async spotify(mountEl, { ref, url }, hooks) {
    457457      const IFrameAPI = await spotifyApi();
     
    473473            const d = e && e.data ? e.data : {};
    474474            hooks.onProgress((d.position || 0) / 1000, (d.duration || 0) / 1000);
    475             // Geen betrouwbare 'ended'-event bij Spotify; we behandelen elke
    476             // isPaused-overgang als play/pause. Einde = gewoon een pauze (de balk
    477             // 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).
    478478            if (d.isPaused === false && lastPaused) { lastPaused = false; hooks.onPlay(); }
    479479            else if (d.isPaused === true && !lastPaused) { lastPaused = true; hooks.onPause(); }
     
    486486
    487487  // ============================================================
    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)
    489489  // ============================================================
    490490  function scan(root) {
     
    492492      if (el.dataset.embedInit) return;
    493493      el.dataset.embedInit = '1';
    494       try { buildCard(el); } catch (e) { console.error('[pcms-embed] buildCard faalde', e); }
     494      try { buildCard(el); } catch (e) { console.error('[pcms-embed] buildCard failed', e); }
    495495    });
    496496  }
     
    501501    scan(document);
    502502  }
    503   // HTMX vervangt #pcms-main bij interne navigatie → opnieuw scannen.
     503  // HTMX replaces #pcms-main on internal navigation → rescan.
    504504  document.body.addEventListener('htmx:afterSwap', (e) => scan(e.target || document));
    505505  document.body.addEventListener('htmx:load', (e) => scan(e.target || document));
    506506
    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.
    512511  const teardownObserver = new MutationObserver((muts) => {
    513512    for (const m of muts) {
  • src/assets/js/smooth-scroll.js

    rbb42dfb r834bcc3  
    22(function () {
    33  if (typeof Lenis === "undefined") return;
    4   // Skip op touch/mobile — native momentum is beter dan JS lib
     4  // Skip on touch/mobile — native momentum scroll is better than a JS lib
    55  var isMobile = window.matchMedia("(pointer: coarse)").matches || window.innerWidth < 1024;
    66  if (isMobile) return;
Note: See TracChangeset for help on using the changeset viewer.