Changeset c0e463b in Klonkt
- Timestamp:
- 08/22/2026 11:05:19 AM (2 weeks ago)
- Branches:
- main
- Children:
- 5e72413
- Parents:
- 4fbe2c1
- Files:
-
- 6 edited
-
src/assets/css/style.css (modified) (1 diff)
-
src/assets/js/audio-player.js (modified) (10 diffs)
-
src/assets/js/mod/tape.js (modified) (4 diffs)
-
src/services/AudioEmbedService.js (modified) (1 diff)
-
src/views/shell.ejs (modified) (3 diffs)
-
test/mixtape-ui.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r4fbe2c1 rc0e463b 5235 5235 background: color-mix(in srgb, var(--accent) 12%, transparent); 5236 5236 } 5237 5238 5239 /* ── De speler in bandmodus ──────────────────────────────────────────── 5240 Zelfde speler, ander gedrag: op een cassette kies je geen nummer. De lijst 5241 blijft staan zodat je ziet wat erop staat, maar zonder de opmaak die belooft 5242 dat je erin kunt prikken. */ 5243 .audio-sheet-queue-list.is-tape .audio-sheet-queue-item { 5244 cursor: default; 5245 pointer-events: none; 5246 } 5247 .audio-sheet-queue-list.is-tape .audio-sheet-queue-item:hover { 5248 background: none; 5249 } 5250 /* Het lopende nummer blijft wel gemarkeerd -- dat is de enige plek waar je op 5251 een bandje leest waar je bent. */ 5252 .audio-sheet-queue-list.is-tape .audio-sheet-queue-item.is-current { 5253 opacity: 1; 5254 } 5255 /* Spoelknoppen mogen niet als "volgende" voelen: vasthouden hoort geen tekst te 5256 selecteren of de pagina te laten zoomen. */ 5257 .audio-btn.is-wind { 5258 touch-action: manipulation; 5259 user-select: none; 5260 } -
src/assets/js/audio-player.js
r4fbe2c1 rc0e463b 27 27 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>', 28 28 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>', 29 32 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>', 30 33 }; … … 128 131 const prevBtn = $('audio-prev'); 129 132 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']; 130 137 const muteBtn = $('audio-mute'); 131 138 const volumeSlider = $('audio-volume'); … … 491 498 } 492 499 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 493 521 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 }; 499 534 } 500 535 return trackTijd(); … … 621 656 albumName = (opts && opts.albumName) || ''; 622 657 tapeMode = !!(opts && opts.asTape); 658 zetKnopStanden(); 623 659 if (!queue.length) return; 624 660 loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true); … … 681 717 albumName = ''; 682 718 tapeMode = false; 719 zetKnopStanden(); 683 720 loadSeq++; // cancel any in-flight load 684 721 dropPreload(); … … 700 737 sheetQueueList.appendChild(li); 701 738 }); 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; 702 744 sheetQueueList.querySelectorAll('.audio-sheet-queue-item').forEach((li) => { 703 745 li.addEventListener('click', () => { … … 883 925 // ============================================================ 884 926 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())); 887 950 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); 890 964 891 965 // ============================================================ … … 1199 1273 } 1200 1274 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 1201 1314 /** Waar staat de teller, over het hele bandje. */ 1202 1315 function tapeTijden() { return displayTimes(); } … … 1204 1317 window.pcmsAudioPlayer = { 1205 1318 setQueue, play, pause, next, prev, close, openSheet, closeSheet, seekBy, tapeTijden, 1319 startWind, stopWind, 1206 1320 isTape: () => tapeMode, 1321 isWinding: () => !!winder, 1207 1322 isPlaying: () => isPlaying, 1208 1323 currentTrack: () => queue[currentIndex] || null, … … 1252 1367 albumName = s.albumName || ''; 1253 1368 tapeMode = !!s.tapeMode; 1369 zetKnopStanden(); 1254 1370 pendingSeek = s.time || 0; 1255 1371 const idx = Math.max(0, Math.min(s.currentIndex || 0, queue.length - 1)); -
src/assets/js/mod/tape.js
r4fbe2c1 rc0e463b 14 14 * die tijdlijn. Vandaar seekBy() en niet next()/prev() -- springen per nummer 15 15 * is een playlistgebaar, en dan is dit een lijst met een cassetteplaatje. 16 * 17 * DE SPOELLUS STAAT IN DE SPELER, niet hier: sinds de speler onderin dezelfde 18 * spoelknoppen heeft zou dezelfde versnelling op twee plekken staan. Deze 19 * module bedient hem en tekent het jasje. 16 20 * 17 21 * WAAROM ER GEPOLLD WORDT. audio-player.js zendt geen gebeurtenissen uit -- het … … 56 60 tape.classList.toggle('is-playing', draait); 57 61 tape.classList.toggle('is-loaded', i >= 0); 62 // Spoelt de SPELER (bijvoorbeeld via zijn eigen knoppen onderin), dan hoort 63 // deze cassette dat ook te laten zien. 64 const windt = i >= 0 && !!p && p.isWinding && p.isWinding(); 65 if (!windt) tape.classList.remove('is-winding', 'is-winding-back'); 66 else tape.classList.add('is-winding'); 58 67 59 68 const knop = tape.querySelector('[data-tape-play]'); … … 91 100 * schermlezer, want vasthouden is met een spatiebalk geen gebaar. 92 101 */ 93 const START_SNELHEID = 4; // maal de normale snelheid bij het indrukken94 const MAX_SNELHEID = 16;95 const VERSNELLING = 1.35; // per stap96 const STAP_MS = 120;97 102 const TIK_STAP_S = 5; // een losse klik 98 103 99 let winder = null;100 101 104 function stopWinden(tape) { 102 if (winder) { clearInterval(winder.timer); winder = null; } 105 const p = speler(); 106 if (p && p.stopWind) p.stopWind(); 103 107 if (tape) tape.classList.remove('is-winding', 'is-winding-back'); 104 108 } … … 106 110 function startWinden(tape, richting) { 107 111 const p = speler(); 108 if (!p || huidigeIndex(tape) < 0) return; 109 stopWinden(tape); 110 let snelheid = START_SNELHEID; 111 let vorigePositie = null; 112 let stil = 0; 112 if (!p || !p.startWind || huidigeIndex(tape) < 0) return; 113 // DE LUS STAAT IN DE SPELER. Hij stond hier, en toen kreeg de speler zelf ook 114 // spoelknoppen -- twee kopieen van dezelfde versnelling die uit elkaar lopen 115 // zodra iemand aan een getal draait. Deze module zet alleen nog het jasje: 116 // welke spoelen draaien en hoe hard. 117 p.startWind(richting); 113 118 tape.classList.add('is-winding'); 114 119 if (richting < 0) tape.classList.add('is-winding-back'); 115 const timer = setInterval(() => {116 snelheid = Math.min(MAX_SNELHEID, snelheid * VERSNELLING);117 p.seekBy(richting * snelheid * (STAP_MS / 1000));118 tekenBand(tape);119 // AAN HET EIND VAN DE BAND STOPPEN MET SPOELEN. De speler klemt de positie120 // af, dus daar gebeurt niets meer -- maar de spoelen bleven wel doordraaien121 // en de knop bleef oplichten, alsof er nog band was. Beweegt de teller twee122 // rondes niet, dan zijn we bij de kop of de staart.123 const nu = p.tapeTijden ? p.tapeTijden().cur : null;124 if (nu !== null && vorigePositie !== null && Math.abs(nu - vorigePositie) < 0.05) {125 if (++stil >= 2) { stopWinden(tape); return; }126 } else stil = 0;127 vorigePositie = nu;128 }, STAP_MS);129 winder = { timer, tape };130 120 } 131 121 -
src/services/AudioEmbedService.js
r4fbe2c1 rc0e463b 562 562 artist: t.artist || pl.artist || '', 563 563 cover: t.cover || pl.cover || '', 564 // De duur gaat mee omdat een BANDJE een lengte heeft. Zonder dit kan de 565 // speler alleen de gebufferde keten optellen, en dan las de teller 566 // 2:05 met een nummer geladen en 4:07 met drie -- een totaal dat 567 // meegroeit terwijl je luistert. 568 duration: Number(t.duration) || 0, 564 569 })); 565 570 const albumJson = JSON.stringify(tracksData) -
src/views/shell.ejs
r4fbe2c1 rc0e463b 222 222 223 223 <!-- v9 stylesheet (full palette system) --> 224 <link rel="stylesheet" href="/assets/css/style.css?v=10 7">224 <link rel="stylesheet" href="/assets/css/style.css?v=108"> 225 225 <script> 226 226 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in … … 406 406 ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat 407 407 Cloudflare (max-age=1y) niet de oude versie blijft serveren. --> 408 <script src="/assets/js/audio-player.js?v=3 7"></script>408 <script src="/assets/js/audio-player.js?v=38"></script> 409 409 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte 410 410 player-API's + gedeelde mutual-exclusion registry met de site-speler. --> … … 505 505 // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig 506 506 // bumpen kost een bugfix die nooit aankomt. 507 var MOD_V = 5 4;507 var MOD_V = 55; 508 508 509 509 // name -> 1 (aan het laden) of de module-namespace (geladen). Een module -
test/mixtape-ui.test.js
r4fbe2c1 rc0e463b 210 210 assert.match(speler, /if \(\(queue\.length === 1 \|\| laatsteVanDeBand\) && ms && ms\.readyState === 'open'\)/); 211 211 }); 212 213 test('de speler-UI schakelt mee in bandmodus (shaer-tmyn)', () => { 214 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8'); 215 216 // 1. De tracklijst blijft staan maar is geen keuzelijst meer. 217 assert.match(speler, /sheetQueueList\.classList\.toggle\('is-tape', tapeMode\);\s*\n\s*if \(tapeMode\) return;/, 218 'zonder deze return kun je op een cassette alsnog naar nummer zeven springen'); 219 220 // 2. Vorige/volgende worden terugspoelen/vooruitspoelen, op EEN plek geregeld. 221 assert.match(speler, /function zetKnopStanden\(\)/); 222 assert.match(speler, /'Vooruitspoelen' : 'Terugspoelen'/); 223 // en vasthouden spoelt door 224 assert.match(speler, /addEventListener\('pointerdown', \(\) => \{ if \(tapeMode\) startWind/); 225 226 // 3. De spoellus staat in de speler, niet meer in de module -- anders lopen 227 // twee kopieen van dezelfde versnelling uit elkaar. 228 assert.match(speler, /function startWind\(richting\)/); 229 const mod = fs.readFileSync('src/assets/js/mod/tape.js', 'utf8'); 230 assert.doesNotMatch(mod, /VERSNELLING/, 'de versnelling hoort niet meer in de module te staan'); 231 assert.match(mod, /p\.startWind\(richting\)/, 'de cassette bedient de gedeelde lus'); 232 }); 233 234 test('de bandteller komt uit de trackduren, niet uit de buffer', () => { 235 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8'); 236 assert.match(speler, /function bandDuur\(\)/); 237 assert.match(speler, /function bandOffset\(i\)/); 238 // Een som met gaten is een verzonnen getal: ontbreekt er een duur, dan valt 239 // hij terug in plaats van te gokken. 240 assert.match(speler, /if \(d <= 0\) return null;/); 241 242 // En de duur moet mee in de wachtrij, anders kan de speler het niet weten. 243 const embed = fs.readFileSync('src/services/AudioEmbedService.js', 'utf8'); 244 assert.match(embed, /duration: Number\(t\.duration\) \|\| 0,/, 245 'zonder dit las de teller de lengte van wat toevallig gebufferd was'); 246 });
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)