timestamps.js (3718B)
1 function parseTimestamp(ts) { 2 const parts = ts.split(":").map(Number).reverse(); 3 let seconds = 0; 4 if (parts[0]) seconds += parts[0]; // seconds 5 if (parts[1]) seconds += parts[1] * 60; // minutes 6 if (parts[2]) seconds += parts[2] * 3600; // hours 7 return seconds; 8 } 9 10 function makeTimestampsClickable(containerId, videoId, imageId) { 11 const container = document.getElementById(containerId); 12 const video = document.getElementById(videoId); 13 const image = document.getElementById(imageId); 14 const videoContainer = document.getElementById('videoContainer'); 15 const imageContainer = document.getElementById('imageContainer'); 16 17 // Regex for timestamps: [h:mm:ss] or [mm:ss] or [ss] 18 const timestampRegex = /\[(\d{1,2}(?::\d{2}){0,2})\]/g; 19 // Regex for rotations: [rotate90], [rotate180], [rotate270], [rotate0] 20 const rotateRegex = /\[rotate(0|90|180|270)\]/g; 21 // Regex for URLs: http(s):// or www. links 22 const urlRegex = /\b((?:https?:\/\/|www\.)[^\s<]+)/gi; 23 // Regex for markdown-style links: [link text](https://example.com) 24 const markdownLinkRegex = /\[([^\[\]]+)\]\(((?:https?:\/\/|www\.)[^)\s]+)\)/gi; 25 26 // Pull out markdown links first and swap in placeholders, so the raw URL 27 // inside them doesn't get double-processed by the bare-URL regex below. 28 const mdLinks = []; 29 let html = container.innerHTML.replace(markdownLinkRegex, (match, text, url) => { 30 const href = url.startsWith("http") ? url : `https://${url}`; 31 const placeholder = `\u0000MDLINK${mdLinks.length}\u0000`; 32 mdLinks.push(`<a href="${href}" class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`); 33 return placeholder; 34 }); 35 36 // Replace bare URLs, then timestamps, then rotations 37 html = html 38 .replace(urlRegex, (match) => { 39 // Strip common trailing punctuation that isn't part of the URL 40 const trailingPunct = /[.,;:!?)\]"'>]+$/; 41 const trimmed = match.match(trailingPunct) ? match.replace(trailingPunct, "") : match; 42 const suffix = match.slice(trimmed.length); 43 const href = trimmed.startsWith("http") ? trimmed : `https://${trimmed}`; 44 return `<a href="${href}" class="external-link" target="_blank" rel="noopener noreferrer">${trimmed}</a>${suffix}`; 45 }) 46 .replace(timestampRegex, (match, ts) => { 47 const seconds = parseTimestamp(ts); 48 return `<a href="#" class="timestamp" data-time="${seconds}">${match}</a>`; 49 }) 50 .replace(rotateRegex, (match, angle) => { 51 return `<a href="#" class="rotate" data-angle="${angle}">${match}</a>`; 52 }); 53 54 // Restore the markdown links now that no other regex can touch them 55 container.innerHTML = html.replace(/\u0000MDLINK(\d+)\u0000/g, (_, i) => mdLinks[Number(i)]); 56 // Handle clicks 57 if (container.dataset.timestampClickBound) return; 58 container.dataset.timestampClickBound = 'true'; 59 60 container.addEventListener("click", e => { 61 if (e.target.classList.contains("timestamp")) { 62 e.preventDefault(); 63 const time = Number(e.target.dataset.time); 64 if (video) { 65 video.currentTime = time; 66 video.play(); 67 window.scrollTo({ top: 0, behavior: "smooth" }); 68 } 69 } else if (e.target.classList.contains("rotate")) { 70 e.preventDefault(); 71 const angle = Number(e.target.dataset.angle); 72 73 if (video) { 74 applyRotation(video, angle); 75 } else if (image) { 76 applyRotation(image, angle); 77 } 78 } 79 }); 80 } 81 82 function applyRotation(element, angle) { 83 element.style.transform = `rotate(${angle}deg)`; 84 element.style.transformOrigin = "center center"; 85 } 86 87 // Run it 88 document.addEventListener("DOMContentLoaded", () => { 89 makeTimestampsClickable("current-description", "videoPlayer", "imageViewer"); 90 });