Changeset 73b41eb in Klonkt


Ignore:
Timestamp:
06/20/2026 03:42:00 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
9e6b54f
Parents:
389aa99
Message:

feat(embed): YouTube audio-only option in posts

Per embed you now choose audio-only or video. [[embed:audio:URL]] (YouTube)
renders as an audio card with our controls (play/seek/volume + thumbnail as
cover); the YT player runs hidden off-screen and provides only the sound.
[[embed:URL]] stays video. The editor embed button asks for audio or video
when a YouTube URL is given. Server: audio:/video: prefix in the shortcode
→ data-embed-mode. busters embed-player.js?v=8, embed.css?v=5.

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

Location:
src
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • src/assets/css/embed.css

    r389aa99 r73b41eb  
    119119  width: 320px;
    120120  height: 60px;
     121  opacity: 0;
     122  pointer-events: none;
     123}
     124/* Audio-only YouTube: de YT-speler draait verborgen (off-screen) maar moet een
     125 * echte grootte houden, anders pauzeert de browser 'm. Alleen het geluid telt. */
     126.pcms-embed-card--youtube.pcms-embed-audio-mode .pcms-embed-mount {
     127  position: absolute;
     128  left: -99999px;
     129  top: 0;
     130  width: 320px;
     131  height: 180px;
    121132  opacity: 0;
    122133  pointer-events: none;
  • src/assets/js/embed-player.js

    r389aa99 r73b41eb  
    183183
    184184    // --- UI ophangen (verschilt per provider-type) ---
    185     const isVideo = provider === 'youtube';
     185    // Audio-only YouTube (data-embed-mode="audio"): render als audio-kaart met
     186    // onze controls; de YT-speler draait verborgen (off-screen) en levert alleen
     187    // het geluid. Video blijft de default.
     188    const audioOnly = provider === 'youtube' && el.dataset.embedMode === 'audio';
     189    if (audioOnly) el.classList.add('pcms-embed-audio-mode');
     190    const isVideo = provider === 'youtube' && !audioOnly;
    186191    const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls
    187192    const poster = provider === 'youtube'
     
    222227    let vol = 100;       // huidige volume 0-100 (wordt op de adapter toegepast)
    223228    let preMuteVol = 100;
     229    // Audio-only YouTube: zet de thumbnail als albumhoes op de audio-kaart.
     230    if (audioOnly && poster) {
     231      const art0 = el.querySelector('.pcms-embed-art');
     232      if (art0) { art0.style.backgroundImage = `url('${poster}')`; art0.classList.add('has-art'); }
     233    }
    224234
    225235    function setPlayingUI(on) {
     
    369379          events: {
    370380            onReady() {
    371               let d = 0; try { d = player.getDuration() || 0; } catch (e) {}
    372               hooks.onReady({ duration: d });
     381              let d = 0; const meta = {};
     382              try { d = player.getDuration() || 0; } catch (e) {}
     383              try { const vd = player.getVideoData(); if (vd && vd.title) meta.title = vd.title; } catch (e) {}
     384              meta.duration = d;
     385              hooks.onReady(meta);
    373386              resolve({
    374387                play() { try { player.playVideo(); } catch (e) {} },
  • src/services/AudioEmbedService.js

    r389aa99 r73b41eb  
    6767      case 'youtube':
    6868        return this.embedPlaceholder('youtube', config.id, 'video',
    69           config.url || `https://youtu.be/${config.id}`);
     69          config.url || `https://youtu.be/${config.id}`, config.mode);
    7070      case 'soundcloud':
    7171        return this.embedPlaceholder('soundcloud', config.url, 'track', config.url);
     
    9191   * ALLE waarden via escape() — post.content_html wordt ongeescaped uitgevoerd.
    9292   */
    93   static embedPlaceholder(provider, ref, type, url) {
     93  static embedPlaceholder(provider, ref, type, url, mode) {
    9494    const attrs = [
    9595      `data-embed-provider="${this.escape(provider)}"`,
    9696      `data-embed-ref="${this.escape(ref)}"`,
    9797      type ? `data-embed-type="${this.escape(type)}"` : '',
     98      mode ? `data-embed-mode="${this.escape(mode)}"` : '',
    9899      `data-embed-url="${this.escape(url)}"`,
    99100    ].filter(Boolean).join(' ');
     
    227228  static embedMediaShortcodes(html) {
    228229    if (!html) return html;
    229     return html.replace(/\[\[embed:([^\]]+)\]\]/gi, (match, rawUrl) => {
    230       const url = rawUrl.trim().replace(/&amp;/g, '&');
     230    return html.replace(/\[\[embed:([^\]]+)\]\]/gi, (match, rawSpec) => {
     231      // Optionele modus-prefix: [[embed:audio:URL]] = alleen-audio speler,
     232      // [[embed:video:URL]] / [[embed:URL]] = normaal (video waar van toepassing).
     233      let spec = rawSpec.trim();
     234      let mode = null;
     235      const mm = spec.match(/^(audio|video):(.+)$/i);
     236      if (mm) { mode = mm[1].toLowerCase(); spec = mm[2].trim(); }
     237      const url = spec.replace(/&amp;/g, '&');
    231238      const detected = this.detectProvider(url);
    232239      if (!detected) {
    233240        return `<div class="post-embed-missing"><em>Embed: niet-ondersteunde of ongeldige URL.</em></div>`;
    234241      }
    235       return this.generateIframe(detected.provider, detected) || match;
     242      return this.generateIframe(detected.provider, { ...detected, mode }) || match;
    236243    });
    237244  }
  • src/views/pages/post-edit.ejs

    r389aa99 r73b41eb  
    14631463      const url = raw.trim();
    14641464      if (!/^https?:\/\//i.test(url)) { alert('Geef een volledige URL (https://…).'); return; }
    1465       insertChip('embed', url);
     1465      // YouTube: keuze audio-only (alleen geluid) of video.
     1466      let val = url;
     1467      if (/(youtube\.com|youtu\.be)/i.test(url)) {
     1468        const audioOnly = window.confirm('YouTube — alleen audio afspelen?\n\nOK = audio-only speler (geen video)\nAnnuleren = video');
     1469        if (audioOnly) val = 'audio:' + url;
     1470      }
     1471      insertChip('embed', val);
    14661472    });
    14671473  }
  • src/views/shell.ejs

    r389aa99 r73b41eb  
    170170<link rel="stylesheet" href="/assets/css/audio.css?v=6">
    171171<!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. -->
    172 <link rel="stylesheet" href="/assets/css/embed.css?v=4">
     172<link rel="stylesheet" href="/assets/css/embed.css?v=5">
    173173
    174174<%- include('partials/shared-styles') %>
     
    303303<!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte
    304304     player-API's + gedeelde mutual-exclusion registry met de site-speler. -->
    305 <script src="/assets/js/embed-player.js?v=7" defer></script>
     305<script src="/assets/js/embed-player.js?v=8" defer></script>
    306306<% if (site && site.enable_audio_player && audioTracks && audioTracks.length > 0) { %>
    307307  <script>window.PCMS_SITE_TRACKS = <%- JSON.stringify(audioTracks) %>;</script>
Note: See TracChangeset for help on using the changeset viewer.