PageSourceSearch

https://teachmephysiology.com/wp-content/plugins/ai-tools/assets/js/frontend.js?ver=1.2.3

js teachmephysiology.com collected 2026-10-01 11:43:33 UTC 12,158 bytes, 371 lines download raw bytes

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.