- Timestamp:
- 08/22/2026 09:32:29 AM (3 weeks ago)
- Branches:
- main
- Children:
- e679bce
- Parents:
- 8e1af9c
- Location:
- src
- Files:
-
- 1 added
- 4 edited
-
assets/css/style.css (modified) (1 diff)
-
assets/js/mod/tape.js (added)
-
routes/posts.js (modified) (2 diffs)
-
services/AudioEmbedService.js (modified) (3 diffs)
-
views/shell.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r8e1af9c r16fd4fe 5076 5076 background: var(--accent, #e8b04b); color: #1a1205; 5077 5077 } 5078 5079 /* ── Het bandje (mixtape) ────────────────────────────────────────────── 5080 Zelfde familie als .post-album hierboven: dezelfde rand, dezelfde radius, 5081 dezelfde papierkleur. Het verschil zit in wat je ziet draaien, niet in een 5082 ander jasje -- een mixtape is een soort uitgave binnen Klonkt, geen ander 5083 product. 5084 5085 Alles hieronder gebruikt de bestaande variabelen (--rule, --paper-2, --ink, 5086 --accent), dus het volgt licht en donker en het accent van de site vanzelf. */ 5087 .post-tape { 5088 position: relative; 5089 border: 1px solid var(--rule, rgba(0,0,0,.1)); 5090 border-radius: 14px; 5091 overflow: hidden; 5092 margin: 1.25rem 0; 5093 background: var(--paper-2, color-mix(in srgb, var(--ink) 3%, transparent)); 5094 } 5095 5096 /* De cassette zelf. De verhouding is die van een echt bandje (ongeveer 10:6), 5097 zodat hij ook op een telefoon als cassette leest en niet als balk. */ 5098 .tape-shell { 5099 display: grid; 5100 grid-template-rows: auto auto; 5101 gap: .5rem; 5102 padding: .8rem .8rem .4rem; 5103 } 5104 .tape-window { 5105 position: relative; 5106 display: flex; 5107 align-items: center; 5108 justify-content: center; 5109 gap: 12%; 5110 padding: .9rem 1.2rem; 5111 border-radius: 10px; 5112 background: color-mix(in srgb, var(--ink) 8%, transparent); 5113 border: 1px solid var(--rule, rgba(0,0,0,.12)); 5114 } 5115 /* De band tussen de spoelen. Een dun lijntje is genoeg: het gaat om de twee 5116 draaiende schijven, dat is wat een cassette een cassette maakt. */ 5117 .tape-band { 5118 position: absolute; 5119 left: 22%; right: 22%; 5120 height: 3px; 5121 background: color-mix(in srgb, var(--ink) 35%, transparent); 5122 border-radius: 2px; 5123 } 5124 .tape-reel { 5125 position: relative; 5126 width: clamp(48px, 16vw, 76px); 5127 aspect-ratio: 1; 5128 border-radius: 50%; 5129 background: 5130 repeating-conic-gradient(from 0deg, 5131 color-mix(in srgb, var(--ink) 22%, transparent) 0deg 12deg, 5132 transparent 12deg 24deg); 5133 border: 2px solid color-mix(in srgb, var(--ink) 30%, transparent); 5134 display: grid; 5135 place-items: center; 5136 } 5137 .tape-reel-hub { 5138 width: 34%; 5139 aspect-ratio: 1; 5140 border-radius: 50%; 5141 background: var(--paper, #fff); 5142 border: 2px solid color-mix(in srgb, var(--ink) 40%, transparent); 5143 } 5144 /* Draaien doet hij alleen als hij speelt. De animatie staat op de spoel en niet 5145 op de hele cassette: het label hoort stil te blijven, anders wordt de titel 5146 onleesbaar. */ 5147 .post-tape.is-playing .tape-reel { animation: tape-spin 2.4s linear infinite; } 5148 .post-tape.is-playing .tape-reel--right { animation-duration: 3.2s; } 5149 @keyframes tape-spin { to { transform: rotate(360deg); } } 5150 /* Wie liever geen beweging ziet, krijgt geen beweging. De cassette blijft dan 5151 gewoon staan; de statusregel eronder vertelt nog steeds wat er speelt. */ 5152 @media (prefers-reduced-motion: reduce) { 5153 .post-tape.is-playing .tape-reel { animation: none; } 5154 } 5155 5156 .tape-label { 5157 padding: .5rem .6rem; 5158 border-radius: 8px; 5159 background-size: cover; 5160 background-position: center; 5161 position: relative; 5162 } 5163 /* Staat er een hoes achter het label, dan moet de tekst leesbaar blijven. */ 5164 .tape-label[style] { color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.7); } 5165 .tape-kind { 5166 margin: 0; 5167 font-size: .7rem; 5168 letter-spacing: .06em; 5169 text-transform: uppercase; 5170 opacity: .75; 5171 } 5172 .tape-title { margin: .1rem 0 0; font-size: 1.05rem; line-height: 1.25; } 5173 .tape-artist { margin: .05rem 0 0; font-size: .85rem; opacity: .8; } 5174 .tape-meta { margin: .15rem 0 0; font-size: .75rem; opacity: .65; } 5175 5176 /* De knoppen: terugspoelen, spelen, vooruitspoelen. Meer heeft een bandje niet, 5177 en dat is de bedoeling -- er is geen zoekbalk omdat je op een cassette niet 5178 kunt zoeken. */ 5179 .tape-controls { 5180 display: flex; 5181 align-items: center; 5182 justify-content: center; 5183 gap: .6rem; 5184 padding: .3rem 0 .7rem; 5185 } 5186 .tape-btn { 5187 display: grid; 5188 place-items: center; 5189 width: 40px; height: 40px; 5190 border-radius: 50%; 5191 border: 1px solid var(--rule, rgba(0,0,0,.15)); 5192 background: var(--paper, #fff); 5193 color: var(--ink); 5194 cursor: pointer; 5195 padding: 0; 5196 } 5197 .tape-btn svg { width: 18px; height: 18px; } 5198 .tape-btn:hover { border-color: var(--accent); color: var(--accent); } 5199 .tape-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } 5200 .tape-btn--play { 5201 width: 52px; height: 52px; 5202 background: var(--accent); 5203 border-color: var(--accent); 5204 color: var(--paper, #fff); 5205 } 5206 .tape-btn--play svg { width: 22px; height: 22px; } 5207 /* Speelt hij, dan is dezelfde knop de pauzeknop. Twee knoppen zou een bandje 5208 niet hebben. */ 5209 .post-tape.is-playing .tape-btn--play svg path { d: path("M7 5h4v14H7zM13 5h4v14h-4z"); } 5210 5211 .tape-now { 5212 margin: 0; 5213 padding: 0 .8rem .4rem; 5214 text-align: center; 5215 font-size: .8rem; 5216 opacity: .8; 5217 min-height: 1.2em; 5218 } 5219 5220 /* De nummers staan er als INHOUD, niet als knoppen. Geen cursor, geen hover, 5221 geen aanwijzing dat je erop kunt drukken: op een cassette kun je dat niet. */ 5222 .tape-tracks { 5223 margin: 0; 5224 padding: 0 .8rem .8rem; 5225 list-style: none; 5226 counter-reset: tape; 5227 display: grid; 5228 gap: .1rem; 5229 } 5230 .tape-track { 5231 counter-increment: tape; 5232 display: flex; 5233 gap: .5rem; 5234 align-items: baseline; 5235 font-size: .85rem; 5236 padding: .18rem .3rem; 5237 border-radius: 6px; 5238 opacity: .75; 5239 } 5240 .tape-track::before { 5241 content: counter(tape) "."; 5242 opacity: .55; 5243 min-width: 1.4em; 5244 font-variant-numeric: tabular-nums; 5245 } 5246 .tape-track-title { flex: 1; } 5247 .tape-track-dur { opacity: .6; font-variant-numeric: tabular-nums; } 5248 .tape-track.is-current { 5249 opacity: 1; 5250 background: color-mix(in srgb, var(--accent) 12%, transparent); 5251 } -
src/routes/posts.js
r8e1af9c r16fd4fe 255 255 bodyClass: 'on-home', 256 256 // mod/read.js: alleen nog de tik-op-een-bericht in de leesweergave. 257 pageJs: 'read ',257 pageJs: 'read tape', 258 258 }); 259 259 }); … … 1695 1695 1696 1696 renderPage(req, res, 'pages/post', { 1697 pageJs: 'post reply-editor ',1697 pageJs: 'post reply-editor tape', 1698 1698 post, 1699 1699 poll: ActivityPubService.ownPollView(post), -
src/services/AudioEmbedService.js
r8e1af9c r16fd4fe 545 545 // uitkomsten, en dan draagt een mixtape het jasje en het woord van een 546 546 // album -- dezelfde vorm die op vier andere plekken al misging. 547 // DE SPELER IS ER NOG NIET. Een mixtape rendert voorlopig als de gewone548 // lijst; wat hier af is, is dat hij zichzelf goed noemt.549 547 const kind = SOORTEN.includes(pl.kind) ? pl.kind : 'album'; 550 548 const KIND_LABEL = { album: '💿 Album', playlist: '📃 Playlist', mixtape: '📼 Mixtape' }; … … 630 628 </li>`; 631 629 }).join('\n'); 630 631 // HET BANDJE HEEFT ZIJN EIGEN VORM. Een album toont een genummerde lijst 632 // waar je in kunt prikken; een cassette is juist het tegenovergestelde -- 633 // je hoort wat er komt, in de volgorde waarin het is opgenomen. Alles 634 // hierboven (de wachtrij, de metaregel, de duur) is gedeeld; alleen de 635 // opmaak splitst hier. 636 if (kind === 'mixtape') { 637 return this.renderTape({ 638 domId: albumDomId, id, titleH, artistH, coverH, metaLine, firstUrl, 639 albumJson, tracks: pl.tracks, isAdmin, 640 }); 641 } 632 642 633 643 return `<div class="post-album" id="${albumDomId}" … … 673 683 674 684 /** 685 * Het bandje (Robins idee, 21-8). 686 * 687 * WAT HET ANDERS MAAKT DAN EEN ALBUM, en dat is de hele reden dat dit een 688 * eigen vorm heeft: bij een album prik je in een genummerde lijst en spring 689 * je naar nummer zeven. Op een cassette kan dat niet. Je spoelt vooruit of 690 * terug, en wat er komt hoor je in de volgorde waarin het is opgenomen. De 691 * lijst staat er dus wel -- je mag zien wat erop staat -- maar hij is geen 692 * knoppenrij. 693 * 694 * DE SPELER IS DE BESTAANDE SPELER. De knop hieronder draagt exact dezelfde 695 * data-attributen als de albumhoes (data-pcms-track-url + data-pcms-album-id), 696 * dus audio-player.js pakt hem op zonder dat hier iets nieuws bij komt. Vooruit 697 * en terug lopen via window.pcmsAudioPlayer.next()/prev(), en dat is meteen de 698 * reden dat spoelen per NUMMER gaat en niet per seconde: die speler denkt in 699 * een wachtrij, en een tweede speler ernaast bouwen om een band na te doen zou 700 * twee dingen tegelijk laten afspelen. 701 */ 702 static renderTape({ domId, id, titleH, artistH, coverH, metaLine, firstUrl, albumJson, tracks, isAdmin }) { 703 // De nummers als tekst, niet als knoppen. Bewust geen data-pcms-track-url: 704 // een aanklikbaar nummer is precies wat een bandje niet heeft. 705 const lijst = tracks.map((t, i) => { 706 const tTitle = this.escape(t.title || ('Track ' + (i + 1))); 707 const dur = t.duration > 0 708 ? `${Math.floor(t.duration / 60)}:${String(t.duration % 60).padStart(2, '0')}` 709 : '—:—'; 710 return ` <li class="tape-track" data-tape-index="${i}"><span class="tape-track-title">${tTitle}</span><span class="tape-track-dur">${dur}</span></li>`; 711 }).join('\n'); 712 713 const spoel = (richting, label, pad) => ` <button type="button" class="tape-btn tape-btn--${richting}" data-tape-go="${richting}" aria-label="${label}"> 714 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">${pad}</svg> 715 </button>`; 716 717 return `<div class="post-tape" id="${domId}" 718 data-pcms-album='${albumJson}' 719 data-pcms-album-title="${titleH}" 720 data-pcms-album-kind="mixtape" 721 data-pcms-playlist-id="${this.escape(id)}"> 722 ${isAdmin ? ` <div class="post-album-actions" role="group" aria-label="Mixtape beheren"> 723 <a class="post-album-action" href="/admin/playlists?edit=${this.escape(id)}" title="Bewerk mixtape" aria-label="Bewerk mixtape"> 724 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg> 725 </a> 726 </div> 727 ` : ''} <div class="tape-shell"> 728 <div class="tape-window" aria-hidden="true"> 729 <span class="tape-reel tape-reel--left"><span class="tape-reel-hub"></span></span> 730 <span class="tape-band"></span> 731 <span class="tape-reel tape-reel--right"><span class="tape-reel-hub"></span></span> 732 </div> 733 <div class="tape-label"${coverH ? ` style="background-image:url('${coverH}')"` : ''}> 734 <p class="tape-kind">📼 Mixtape</p> 735 <h3 class="tape-title">${titleH}</h3> 736 ${artistH ? `<p class="tape-artist">${artistH}</p>` : ''} 737 <p class="tape-meta">${metaLine}</p> 738 </div> 739 </div> 740 <div class="tape-controls" role="group" aria-label="Bandje bedienen"> 741 ${spoel('back', 'Terugspoelen', '<path d="M11 12l9-7v14zM2 12l9-7v14z"/>')} 742 <button type="button" class="tape-btn tape-btn--play" 743 data-pcms-track-url="${firstUrl}" 744 data-pcms-album-id="${domId}" 745 data-tape-play 746 aria-label="Afspelen"> 747 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg> 748 </button> 749 ${spoel('fwd', 'Vooruitspoelen', '<path d="M13 12L4 5v14zM22 12l-9-7v14z"/>')} 750 </div> 751 <p class="tape-now" data-tape-now aria-live="polite"></p> 752 <ol class="tape-tracks"> 753 ${lijst} 754 </ol> 755 </div>`; 756 } 757 758 /** 675 759 * Human-readable label for a provider slug. Used by external-link buttons. 676 760 */ -
src/views/shell.ejs
r8e1af9c r16fd4fe 222 222 223 223 <!-- v9 stylesheet (full palette system) --> 224 <link rel="stylesheet" href="/assets/css/style.css?v=10 5">224 <link rel="stylesheet" href="/assets/css/style.css?v=106"> 225 225 <script> 226 226 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in … … 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 1;507 var MOD_V = 52; 508 508 509 509 // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
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)