Changeset b5a09ce2 in Klonkt


Ignore:
Timestamp:
06/30/2026 05:32:33 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
857a06f
Parents:
1d6f9a2
Message:

feat(video-cover): frontend — render animated covers as iOS video, capture in editor

  • post-edit.ejs: hidden cover_video_url field; the cover uploader stores the returned MP4 url
  • post.ejs: the cover <img> carries data-ios-mp4 when a loop MP4 exists
  • shell.ejs: on iOS, swap <img data-ios-mp4> for a muted looping inline <video> (runs on load + htmx swaps); every other browser keeps the crisp WebP

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

Location:
src/views
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/post-edit.ejs

    r1d6f9a2 rb5a09ce2  
    106106                   placeholder="<%= t('pedit.cover_url_placeholder') %>">
    107107          </label>
     108          <%# Muted loop MP4 for an animated cover (auto-made from an animated WebP). Hidden — set by the uploader. %>
     109          <input type="hidden" name="cover_video_url" id="cover-video-field" value="<%= post.cover_video_url || '' %>">
    108110          <div class="pe-cover-upload">
    109111            <button type="button" class="pe-btn pe-btn-secondary" id="cover-upload-trigger">
     
    11701172  const coverTrigger = document.getElementById('cover-upload-trigger');
    11711173  const coverUrl     = document.getElementById('cover-url-field');
     1174  const coverVideo   = document.getElementById('cover-video-field');
    11721175  const coverStatus  = document.getElementById('cover-upload-status');
    11731176  const coverWrap    = document.getElementById('cover-preview-wrap');
     
    13291332        const j = await uploadImage(edited);
    13301333        coverUrl.value = j.url;
     1334        if (coverVideo) coverVideo.value = j.video || ''; // muted loop MP4 for an animated cover
    13311335        showCoverPreview(j.url);
    1332         coverStatus.textContent = '<%= t('pedit.js_uploaded') %> ✓';
     1336        coverStatus.textContent = (j.video ? '🎬 ' : '') + '<%= t('pedit.js_uploaded') %> ✓';
    13331337        setTimeout(() => { coverStatus.textContent = ''; }, 2000);
    13341338      } catch (e) {
  • src/views/pages/post.ejs

    r1d6f9a2 rb5a09ce2  
    2020  <% if (post.cover_image_url) { %>
    2121    <figure class="post-cover">
    22       <img src="<%= post.cover_image_url %>" alt="">
     22      <img src="<%= post.cover_image_url %>" alt=""<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>>
    2323    </figure>
    2424  <% } %>
  • src/views/shell.ejs

    r1d6f9a2 rb5a09ce2  
    771771  document.body.addEventListener('htmx:afterSettle', function (e) { scan(e.target); });
    772772})();
     773
     774// iOS animated-cover → video: an animated WebP is janky on iOS Safari, so on iOS we swap any
     775// <img data-ios-mp4="…"> for a muted, looping, inline <video> (the WebP's matching MP4). Every
     776// other browser keeps the crisp WebP. Runs on load + htmx swaps.
     777(function () {
     778  if (window.__iosVideoWired) return; window.__iosVideoWired = true;
     779  var ua = navigator.userAgent || '';
     780  var IS_IOS = /iP(hone|od|ad)/.test(navigator.platform || '') || /iPad|iPhone|iPod/.test(ua) ||
     781               (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1); // iPadOS reports as Mac
     782  if (!IS_IOS) return;
     783  function swap(root) {
     784    (root || document).querySelectorAll('img[data-ios-mp4]').forEach(function (img) {
     785      var mp4 = img.getAttribute('data-ios-mp4');
     786      if (!mp4 || img.dataset.iosSwapped) return;
     787      img.dataset.iosSwapped = '1';
     788      var v = document.createElement('video');
     789      v.src = mp4; v.muted = true; v.loop = true; v.autoplay = true;
     790      v.setAttribute('muted', ''); v.setAttribute('playsinline', ''); v.setAttribute('webkit-playsinline', '');
     791      v.poster = img.getAttribute('src') || '';
     792      v.className = img.className;
     793      if (img.getAttribute('style')) v.setAttribute('style', img.getAttribute('style'));
     794      if (img.parentNode) img.parentNode.replaceChild(v, img);
     795      var p = v.play && v.play(); if (p && p.catch) p.catch(function () {});
     796    });
     797  }
     798  swap(document);
     799  document.body.addEventListener('htmx:afterSettle', function (e) { swap(e.target); });
     800})();
    773801</script>
    774802
Note: See TracChangeset for help on using the changeset viewer.