Changeset c0e463b in Klonkt for src/assets/js/audio-player.js


Ignore:
Timestamp:
08/22/2026 11:05:19 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
5e72413
Parents:
4fbe2c1
Message:

Speler-UI in bandmodus: spoelknoppen, eerlijke teller, lijst zonder keuze (shaer-tmyn)

Speelt er een bandje, dan hoort de speler onderin zich als een cassettedeck te
gedragen en niet als een playlist-speler. Drie van de vier stappen uit de bead;
de vierde (seek over de hele tijdlijn) staat er los van, want die raakt
loadTrack en de keten.

  • De tracklijst blijft staan maar is geen keuzelijst: renderQueue hangt in bandmodus geen klik meer op de items, en een CSS-klasse haalt de belofte weg dat het kan. Op een cassette spring je niet naar nummer zeven.
  • Vorige/volgende worden terugspoelen/vooruitspoelen: ander teken, ander woord, en vasthouden spoelt door. Een korte klik spoelt vijf seconden, voor toetsenbord en schermlezer.
  • De spoellus is VERHUISD naar de speler. Hij stond in mod/tape.js, en met spoelknoppen op twee plekken zouden twee kopieen van dezelfde versnelling uit elkaar lopen zodra iemand aan een getal draait. De cassette in de post bedient nu dezelfde lus en tekent alleen nog het jasje.

En de teller is eerlijk geworden. Die kwam uit chain[last].end, dus uit de
lengte van wat toevallig gebufferd was: 2:05 met een nummer geladen, 4:07 met
drie, terwijl de band 6:13 duurt. Erger nog, sprong je naar nummer drie dan
begon de keten daar opnieuw op nul en stond de teller weer vooraan. Nu telt hij
bandOffset(nummer) + positie in dat nummer, uit de trackduren -- die daarvoor
in de wachtrij-JSON moesten. Ontbreekt er een duur, dan valt hij terug: een som
met gaten is een verzonnen getal.

Nagemeten op dev: totaal 6:13 meteen bij de eerste seconde (klopt met de
PT373S op het AP-object), vier knoppen op Terug-/Vooruitspoelen, drie items in
de lijst met pointer-events none en het lopende nummer gemarkeerd, en spoelen
via de spelerknop loopt door de band heen.

audio-player v38, MOD_V 55, style.css v108.

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

File:
1 edited

Legend:

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

    r4fbe2c1 rc0e463b  
    2727    vol:   '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zM16 8a5 5 0 010 8M19 5a9 9 0 010 14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>',
    2828    mute:  '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zM17 9l5 5M22 9l-5 5" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>',
     29    // Dubbele driehoeken: het teken voor spoelen, niet voor overslaan.
     30    rew:   '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 12l9-7v14zM2 12l9-7v14z" fill="currentColor"/></svg>',
     31    ff:    '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 12L4 5v14zM22 12l-9-7v14z" fill="currentColor"/></svg>',
    2932    musicNote: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 17V5l12-2v12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="17" r="3" fill="currentColor"/><circle cx="18" cy="15" r="3" fill="currentColor"/></svg>',
    3033  };
     
    128131  const prevBtn = $('audio-prev');
    129132  const nextBtn = $('audio-next');
     133  // De vier knoppen die in bandmodus van betekenis veranderen: vorige/volgende
     134  // worden terugspoelen/vooruitspoelen. Ze staan hier bij elkaar zodat het
     135  // omzetten op EEN plek gebeurt.
     136  const SPOELKNOPPEN = ['audio-prev', 'audio-next', 'audio-sheet-prev', 'audio-sheet-next'];
    130137  const muteBtn = $('audio-mute');
    131138  const volumeSlider = $('audio-volume');
     
    491498  }
    492499
     500  // De lengte van de hele band, uit de bekende trackduren. Null zodra er van
     501  // een nummer geen duur bekend is: een som met gaten is een verzonnen getal,
     502  // en dan valt de teller liever terug op wat hij wel zeker weet.
     503  function bandDuur() {
     504    if (!queue.length) return null;
     505    let som = 0;
     506    for (const t of queue) {
     507      const d = Number(t && t.duration) || 0;
     508      if (d <= 0) return null;
     509      som += d;
     510    }
     511    return som;
     512  }
     513
     514  /** Hoeveel band ligt er voor nummer `i`. */
     515  function bandOffset(i) {
     516    let som = 0;
     517    for (let n = 0; n < i && n < queue.length; n++) som += Number(queue[n].duration) || 0;
     518    return som;
     519  }
     520
    493521  function displayTimes() {
    494     // Een bandje heeft een teller, geen nummerpositie: hij telt door over de
    495     // kant heen. Dat is letterlijk de ketentijdlijn, dus hier juist NIET
    496     // terugrekenen naar trackcoordinaten.
    497     if (tapeMode && useMse() && chain.length) {
    498       return { cur: audio.currentTime || 0, dur: chain[chain.length - 1].end };
     522    // EEN BANDJE HEEFT EEN TELLER, geen nummerpositie. Hij telt door over de
     523    // kant heen.
     524    //
     525    // Uit de trackduren en niet uit de keten, en dat verschil is zichtbaar: de
     526    // keten bevat alleen wat gebufferd is, dus het totaal groeide mee tijdens
     527    // het luisteren (2:05 met een nummer geladen, 4:07 met drie). En sprong je
     528    // naar nummer drie, dan begon de keten daar opnieuw op nul en stond de
     529    // teller weer aan het begin van de band.
     530    if (tapeMode) {
     531      const totaal = bandDuur();
     532      if (totaal) return { cur: bandOffset(currentIndex) + (trackTijd().cur || 0), dur: totaal };
     533      if (useMse() && chain.length) return { cur: audio.currentTime || 0, dur: chain[chain.length - 1].end };
    499534    }
    500535    return trackTijd();
     
    621656    albumName = (opts && opts.albumName) || '';
    622657    tapeMode = !!(opts && opts.asTape);
     658    zetKnopStanden();
    623659    if (!queue.length) return;
    624660    loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true);
     
    681717    albumName = '';
    682718    tapeMode = false;
     719    zetKnopStanden();
    683720    loadSeq++;  // cancel any in-flight load
    684721    dropPreload();
     
    700737      sheetQueueList.appendChild(li);
    701738    });
     739    // OP EEN BANDJE KIES JE NIET. De lijst blijft staan -- je mag zien wat
     740    // erop staat -- maar zonder klik en zonder de opmaak die belooft dat het
     741    // kan. Dat is hetzelfde onderscheid als bij de cassette in de post.
     742    sheetQueueList.classList.toggle('is-tape', tapeMode);
     743    if (tapeMode) return;
    702744    sheetQueueList.querySelectorAll('.audio-sheet-queue-item').forEach((li) => {
    703745      li.addEventListener('click', () => {
     
    883925  // ============================================================
    884926  playBtn.addEventListener('click', togglePlay);
    885   prevBtn.addEventListener('click', prev);
    886   nextBtn.addEventListener('click', next);
     927  /**
     928   * De vier knoppen in de juiste stand zetten. In bandmodus zijn het
     929   * spoelknoppen: ander teken, ander woord, en vasthouden spoelt door.
     930   */
     931  function zetKnopStanden() {
     932    for (const id of SPOELKNOPPEN) {
     933      const b = $(id);
     934      if (!b) continue;
     935      const vooruit = id.endsWith('next');
     936      b.innerHTML = tapeMode ? (vooruit ? SVG.ff : SVG.rew) : (vooruit ? SVG.next : SVG.prev);
     937      const label = tapeMode ? (vooruit ? 'Vooruitspoelen' : 'Terugspoelen') : (vooruit ? 'Volgende' : 'Vorige');
     938      b.setAttribute('aria-label', label);
     939      b.setAttribute('title', label);
     940      b.classList.toggle('is-wind', tapeMode);
     941    }
     942  }
     943
     944  // Een KLIK: een nummer verder, of in bandmodus een stukje spoelen. Dat laatste
     945  // is er voor toetsenbord en schermlezer, want vasthouden is met een
     946  // spatiebalk geen gebaar.
     947  const TIK_SPOEL_S = 5;
     948  prevBtn.addEventListener('click', () => (tapeMode ? seekBy(-TIK_SPOEL_S) : prev()));
     949  nextBtn.addEventListener('click', () => (tapeMode ? seekBy(TIK_SPOEL_S) : next()));
    887950  sheetPlay.addEventListener('click', togglePlay);
    888   sheetPrev.addEventListener('click', prev);
    889   sheetNext.addEventListener('click', next);
     951  sheetPrev.addEventListener('click', () => (tapeMode ? seekBy(-TIK_SPOEL_S) : prev()));
     952  sheetNext.addEventListener('click', () => (tapeMode ? seekBy(TIK_SPOEL_S) : next()));
     953
     954  // VASTHOUDEN spoelt door. Op de knop zelf indrukken, maar loslaten op
     955  // document: glijdt je vinger van de knop af, dan hoort de band te stoppen en
     956  // niet door te blijven spoelen.
     957  for (const id of SPOELKNOPPEN) {
     958    const b = $(id);
     959    if (!b) continue;
     960    b.addEventListener('pointerdown', () => { if (tapeMode) startWind(id.endsWith('next') ? 1 : -1); });
     961  }
     962  document.addEventListener('pointerup', stopWind);
     963  document.addEventListener('pointercancel', stopWind);
    890964
    891965  // ============================================================
     
    11991273  }
    12001274
     1275  /**
     1276   * SPOELEN, met de knop ingedrukt.
     1277   *
     1278   * Stond eerst in assets/js/mod/tape.js, want daar zat de cassette. Nu de
     1279   * speler zelf ook spoelknoppen krijgt zou dezelfde lus op twee plekken staan,
     1280   * en dan lopen ze uit elkaar zodra iemand aan de versnelling draait. Hier is
     1281   * de enige plek; de cassette op de pagina roept deze aan.
     1282   */
     1283  const WIND_START = 4;        // maal de normale snelheid bij het indrukken
     1284  const WIND_MAX = 16;
     1285  const WIND_VERSNELLING = 1.35;
     1286  const WIND_STAP_MS = 120;
     1287  let winder = null;
     1288
     1289  function stopWind() {
     1290    if (winder) { clearInterval(winder.timer); winder = null; }
     1291  }
     1292
     1293  function startWind(richting) {
     1294    stopWind();
     1295    const r = richting < 0 ? -1 : 1;
     1296    let snelheid = WIND_START;
     1297    let vorige = null;
     1298    let stil = 0;
     1299    const timer = setInterval(() => {
     1300      snelheid = Math.min(WIND_MAX, snelheid * WIND_VERSNELLING);
     1301      seekBy(r * snelheid * (WIND_STAP_MS / 1000));
     1302      // AAN DE KOP EN DE STAART STOPPEN. De positie wordt afgeklemd, dus daar
     1303      // gebeurt niets meer -- maar zonder deze controle blijft de knop malen en
     1304      // draaien de spoelen door alsof er nog band is.
     1305      const nu = displayTimes().cur;
     1306      if (vorige !== null && Math.abs(nu - vorige) < 0.05) {
     1307        if (++stil >= 2) { stopWind(); return; }
     1308      } else stil = 0;
     1309      vorige = nu;
     1310    }, WIND_STAP_MS);
     1311    winder = { timer, richting: r };
     1312  }
     1313
    12011314  /** Waar staat de teller, over het hele bandje. */
    12021315  function tapeTijden() { return displayTimes(); }
     
    12041317  window.pcmsAudioPlayer = {
    12051318    setQueue, play, pause, next, prev, close, openSheet, closeSheet, seekBy, tapeTijden,
     1319    startWind, stopWind,
    12061320    isTape: () => tapeMode,
     1321    isWinding: () => !!winder,
    12071322    isPlaying: () => isPlaying,
    12081323    currentTrack: () => queue[currentIndex] || null,
     
    12521367    albumName = s.albumName || '';
    12531368    tapeMode = !!s.tapeMode;
     1369    zetKnopStanden();
    12541370    pendingSeek = s.time || 0;
    12551371    const idx = Math.max(0, Math.min(s.currentIndex || 0, queue.length - 1));
Note: See TracChangeset for help on using the changeset viewer.