1jQuery(document).ready(function () { 2 const playerI18n = window.audioPlayerI18n || { 3 playAudio: 'Play audio', 4 pauseAudio: 'Pause audio' 5 }; 6 7 const $audioElement = jQuery('#audioElement'); 8 const $playBtn = jQuery('#playBtn'); 9 const $progressBar = jQuery('#progressBar'); 10 const $progressFill = jQuery('#progressFill'); 11 const $bufferFill = jQuery('#bufferFill'); 12 const $timeDisplay = jQuery('#timeDisplay'); 13 const $audioPlayer = jQuery('#audioPlayer'); 14 const $audioPlayerPlaceholder = jQuery('#audioPlayerPlaceholder'); 15 const $speedToggle = jQuery('#speedToggle'); 16 const $speedMenu = jQuery('#speedMenu'); 17 const $speedOptions = $speedMenu.find('button'); 18 19 const audioNode = $audioElement[0]; 20 const DEFAULT_SPEED = 1; 21 const SPEED_STORAGE_KEY = 'audio_playback_speed'; 22 const isLoggedIn = jQuery('body').hasClass('logged-in'); 23 24 let isPlaying = false; 25 let hasBeenPlayed = false; 26 let audioPlayerOriginalPosition = null; 27 let isAudioPlayerSticky = false; 28 let preferredSpeed = DEFAULT_SPEED; 29 30 function loadPreferredSpeed() { 31 const localSpeed = localStorage.getItem(SPEED_STORAGE_KEY); 32 33 if (localSpeed) { 34 preferredSpeed = parseFloat(localSpeed); 35 applyPreferredSpeed(); 36 return; 37 } 38 39 if (isLoggedIn && typeof audioAnalyticsData !== 'undefined' && audioAnalyticsData.ajaxUrl) { 40 jQuery.post(audioAnalyticsData.ajaxUrl, { 41 action: 'get_audio_speed_preference' 42 }).done(function (response) { 43 if (response.success && response.data && response.data.speed) { 44 preferredSpeed = response.data.speed; 45 // Store in localStorage for future visits 46 localStorage.setItem(SPEED_STORAGE_KEY, preferredSpeed); 47 applyPreferredSpeed(); 48 } 49 }); 50 } 51 } 52 53 function applyPreferredSpeed() { 54 if (preferredSpeed && preferredSpeed !== DEFAULT_SPEED) { 55 audioNode.playbackRate = preferredSpeed;
56 setActiveSpeed(preferredSpeed); 57 } 58 } 59 60 function savePreferredSpeed(speed) { 61 localStorage.setItem(SPEED_STORAGE_KEY, speed); 62 63 if (isLoggedIn && typeof audioAnalyticsData !== 'undefined' && audioAnalyticsData.ajaxUrl) { 64 jQuery.post(audioAnalyticsData.ajaxUrl, { 65 action: 'save_audio_speed_preference', 66 speed: speed 67 }); 68 } 69 } 70 71 let analytics = null; 72 if (typeof audioAnalyticsData !== 'undefined' && audioAnalyticsData.postId && audioAnalyticsData.ajaxUrl && audioAnalyticsData.nonce) { 73 analytics = new AudioAnalytics( 74 audioAnalyticsData.postId, 75 audioAnalyticsData.ajaxUrl, 76 audioAnalyticsData.nonce 77 ); 78 79 const resumeFromPosition = () => { 80 if (!analytics || !audioNode) return; 81 82 const resumePercent = analytics.getResumePosition(); 83 if (resumePercent > 0 && audioNode.duration) { 84 const resumeTime = (resumePercent / 100) * audioNode.duration; 85 audioNode.currentTime = resumeTime; 86 } 87 }; 88 89 if (audioNode.readyState >= 1) { 90 resumeFromPosition(); 91 } else { 92 $audioElement.one('loadedmetadata', resumeFromPosition); 93 } 94 } 95 96 // Load preferred speed on page load 97 loadPreferredSpeed(); 98 99 function formatTime(seconds) { 100 if (!Number.isFinite(seconds) || seconds < 0) { 101 return '0:00'; 102 } 103 const mins = Math.floor(seconds / 60); 104 const secs = Math.floor(seconds % 60); 105 return `${mins}:${secs.toString().padStart(2, '0')}`; 106 } 107 108 function updateBufferProgress() { 109 if (!audioNode.buffered || audioNode.buffered.length === 0) { 110 return; 111 } 112 113 const duration = audioNode.duration; 114 if (!Number.isFinite(duration) || duration <= 0) { 115 return; 116 } 117 118 // Get the end of the last buffered range 119 let bufferedEnd = 0; 120 for (let i = 0; i < audioNode.buffered.length; i++) { 121 const end = audioNode.buffered.end(i); 122 if (end > bufferedEnd) { 123 bufferedEnd = end; 124 } 125 } 126 127 const bufferProgress = (bufferedEnd / duration) * 100; 128 $bufferFill.css('width', `${bufferProgress}%`); 129 } 130 131 function formatSpeedLabel(speed) { 132 if (!Number.isFinite(speed) || speed <= 0) { 133 return `${DEFAULT_SPEED}x`; 134 } 135 return (Number.isInteger(speed) ? speed.toFixed(0) : speed.toString()) + 'x'; 136 } 137 138 function setActiveSpeed(speed) { 139 const label = formatSpeedLabel(speed); 140 $speedToggle.text(label); 141 142 $speedOptions.removeClass('is-active'); 143 $speedOptions.each(function () { 144 const optionSpeed = parseFloat(jQuery(this).data('speed')); 145 if (Math.abs(optionSpeed - speed) < 0.0001) { 146 jQuery(this).addClass('is-active'); 147 } 148 }); 149 } 150 151 function openSpeedMenu() { 152 $speedMenu.addClass('open'); 153 $speedToggle.attr('aria-expanded', 'true'); 154 } 155 156 function closeSpeedMenu() { 157 $speedMenu.removeClass('open'); 158 $speedToggle.attr('aria-expanded', 'false'); 159 } 160 161 function updatePlaybackState(playing) { 162 isPlaying = !!playing; 163 if (isPlaying) { 164 $playBtn.addClass('is-playing'); 165 $playBtn.attr({ 'aria-label': playerI18n.pauseAudio, 'aria-pressed': 'true' }); 166 } else { 167 $playBtn.removeClass('is-playing'); 168 $playBtn.attr({ 'aria-label': playerI18n.playAudio, 'aria-pressed': 'false' }); 169 } 170 checkAudioPlayerSticky(); 171 } 172 173 $playBtn.on('click', function () { 174 if (audioNode.paused) { 175 const playPromise = audioNode.play(); 176 if (playPromise && typeof playPromise.then === 'function') { 177 playPromise.then(() => updatePlaybackState(true)).catch(() => updatePlaybackState(false)); 178 } else { 179 updatePlaybackState(true); 180 } 181 } else { 182 audioNode.pause(); 183 updatePlaybackState(false); 184 } 185 }); 186 $audioElement.on('loadedmetadata', function () { 187 const duration = formatTime(this.duration); 188 $timeDisplay.text(`${formatTime(0)} / ${duration}`); 189 updateBufferProgress(); 190 }); 191 if (audioNode.readyState >= 1) { 192 const duration = formatTime(audioNode.duration); 193 $timeDisplay.text(`${formatTime(0)} / ${duration}`); 194 updateBufferProgress(); 195 } 196 $audioElement.on('timeupdate', function () { 197 const audio = this; 198 const durationValue = Number.isFinite(audio.duration) && audio.duration > 0 ? audio.duration : 0; 199 const progress = durationValue ? (audio.currentTime / durationValue) * 100 : 0; 200 $progressFill.css('width', `${progress}%`); 201 202 const currentTime = formatTime(audio.currentTime);
203 const duration = formatTime(durationValue); 204 $timeDisplay.text(`${currentTime} / ${duration}`); 205 206 // Track analytics 207 if (analytics) { 208 analytics.onTimeUpdate(audio.currentTime, durationValue); 209 } 210 }); 211 212 // Update buffer progress as audio loads 213 $audioElement.on('progress', updateBufferProgress); 214 215 $progressBar.on('click', function (e) { 216 const clickX = e.offsetX; 217 const width = jQuery(this).width(); 218 const targetDuration = Number.isFinite(audioNode.duration) && audioNode.duration > 0 ? audioNode.duration : 0; 219 if (!targetDuration) { 220 return; 221 } 222 const newTime = (clickX / width) * targetDuration; 223 audioNode.currentTime = newTime; 224 225 // Track seek 226 if (analytics) { 227 analytics.onSeek(); 228 } 229 }); 230 231 function initAudioPlayerPosition() { 232 const rect = $audioPlayer[0].getBoundingClientRect(); 233 audioPlayerOriginalPosition = rect.top + jQuery(window).scrollTop(); 234 } 235 236 function checkAudioPlayerSticky() { 237 if (!audioPlayerOriginalPosition) { 238 initAudioPlayerPosition(); 239 return; 240 } 241 242 const scrollTop = jQuery(window).scrollTop(); 243 const shouldBeSticky = scrollTop > audioPlayerOriginalPosition + 20 && hasBeenPlayed; 244 245 if (shouldBeSticky && !isAudioPlayerSticky) { 246 isAudioPlayerSticky = true; 247 $audioPlayer.addClass('sticky'); 248 $audioPlayerPlaceholder.addClass('active'); 249 250 updateScrollingTitle(); 251 } else if (!shouldBeSticky && isAudioPlayerSticky) { 252 isAudioPlayerSticky = false; 253 $audioPlayer.removeClass('sticky'); 254 $audioPlayerPlaceholder.removeClass('active'); 255 } 256 } 257 258 jQuery(window).on('scroll', checkAudioPlayerSticky); 259 jQuery(window).on('load', initAudioPlayerPosition); 260 261 $audioElement.on('play', function () { 262 hasBeenPlayed = true; 263 updatePlaybackState(true); 264 265 // Track play 266 if (analytics) { 267 analytics.onPlay(); 268 } 269 }); 270 271 $audioElement.on('pause', function () { 272 // Avoid firing on pause event triggered by ending until ended handler runs 273 if (!audioNode.ended) { 274 updatePlaybackState(false); 275 276 // Track pause 277 if (analytics) { 278 analytics.onPause(); 279 } 280 } 281 }); 282 283 $audioElement.on('ended', function () { 284 updatePlaybackState(false); 285 $progressFill.css('width', '0%'); 286 287 // Track ended 288 if (analytics) { 289 analytics.onEnded(); 290 } 291 }); 292 293 $speedToggle.on('click', function (event) { 294 event.preventDefault(); 295 event.stopPropagation(); 296 297 if ($speedMenu.hasClass('open')) { 298 closeSpeedMenu(); 299 } else { 300 openSpeedMenu(); 301 } 302 }); 303 304 $speedOptions.on('click', function (event) { 305 event.preventDefault(); 306 event.stopPropagation(); 307 308 const newSpeed = parseFloat(jQuery(this).data('speed')); 309 if (!Number.isFinite(newSpeed) || newSpeed <= 0) { 310 return; 311 } 312 313 audioNode.playbackRate = newSpeed; 314 setActiveSpeed(newSpeed); 315 closeSpeedMenu(); 316 317 // Save preferred speed 318 savePreferredSpeed(newSpeed); 319 320 // Track speed change 321 if (analytics) { 322 analytics.onSpeedChange(newSpeed); 323 } 324 }); 325 326 $audioElement.on('ratechange', function () { 327 setActiveSpeed(this.playbackRate || DEFAULT_SPEED); 328 }); 329 330 jQuery(document).on('click', function (event) { 331 if (!jQuery(event.target).closest('.audio-speed').length) { 332 closeSpeedMenu(); 333 } 334 }); 335 336 jQuery(document).on('keydown', function (event) { 337 if (event.key === 'Escape') { 338 closeSpeedMenu(); 339 } 340 }); 341 342 function updateScrollingTitle() { 343 const $player = jQuery('#audioPlayer'); 344 345 if (!$player.hasClass('sticky')) return; // only run if sticky 346 347 const titleWrapper = $player.find('.audio-title')[0]; 348 const animatedSpan = titleWrapper.querySelector('.animate'); 349
350 animatedSpan.style.animation = 'none'; 351 animatedSpan.classList.remove('no-overflow'); 352 animatedSpan.style.transform = 'none'; 353 354 requestAnimationFrame(() => { 355 const overflowAmount = animatedSpan.scrollWidth - titleWrapper.clientWidth; 356 357 if (overflowAmount > 0) { 358 animatedSpan.style.setProperty('--scroll-distance', `${overflowAmount}px`); 359 void animatedSpan.offsetWidth; // force reflow 360 animatedSpan.style.animation = ''; // re-trigger CSS animation 361 } else { 362 animatedSpan.classList.add('no-overflow'); 363 } 364 }); 365 } 366 367 // Initialise defaults 368 updatePlaybackState(!audioNode.paused && !audioNode.ended); 369 setActiveSpeed(audioNode.playbackRate || DEFAULT_SPEED); 370 closeSpeedMenu(); 371});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.