1/** 2 * Reggie Audiobook Player â Frontend JS 3 * Bookmarking uses localStorage only â no login required. 4 */ 5(function ($) { 6 'use strict'; 7 8 /* ââ Helpers âââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 9 function fmtTime(sec) { 10 sec = Math.floor(sec || 0); 11 var h = Math.floor(sec / 3600); 12 var m = Math.floor((sec % 3600) / 60); 13 var s = sec % 60; 14 if (h > 0) return h + ':' + pad(m) + ':' + pad(s); 15 return m + ':' + pad(s); 16 } 17 function pad(n) { return n < 10 ? '0' + n : n; } 18 19 /* ââ Bookmark: localStorage only ââââââââââââââââââââââââââââââââââ */ 20 function bmKey(bookId) { return 'rap_bm_' + bookId; } 21 22 function bmSave(bookId, chapterIndex, timestamp) { 23 if (timestamp <= 0) return; 24 try { 25 localStorage.setItem(bmKey(bookId), JSON.stringify({ 26 chapter_index: chapterIndex, 27 timestamp: timestamp 28 })); 29 } catch(e) {} 30 } 31 32 function bmGet(bookId) { 33 try { 34 var raw = localStorage.getItem(bmKey(bookId)); 35 return raw ? JSON.parse(raw) : null; 36 } catch(e) { return null; } 37 } 38 39 /* ââ Player init âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 40 function initPlayer(wrap) { 41 var bookId = wrap.data('book-id'); 42 var audio = document.getElementById('rap-audio-' + bookId); 43 if (!audio) return; 44 var $audio = $(audio); 45 46 var $chapters = wrap.find('.rap-chapter-item'); 47 var $fill = wrap.find('.rap-progress-fill'); 48 var $thumb = wrap.find('.rap-progress-thumb'); 49 var $bar = wrap.find('.rap-progress-bar'); 50 var $curTime = wrap.find('.rap-time-current'); 51 var $totalTime = wrap.find('.rap-time-total'); 52 var $playPause = wrap.find('.rap-play-pause'); 53 var $iconPlay = wrap.find('.rap-icon-play'); 54 var $iconPause = wrap.find('.rap-icon-pause'); 55 var $speed = wrap.find('.rap-speed'); 56 var $volume = wrap.find('.rap-volume'); 57 var $status = wrap.find('.rap-bookmark-status'); 58 59 var currentChapter = 0; 60 var isSeeking = false; 61 var statusTimer = null; 62 var saveTimer = null; 63 64 function showStatus(msg, duration) { 65 $status.text(msg).show(); 66 clearTimeout(statusTimer); 67 if (duration) statusTimer = setTimeout(function () { $status.fadeOut(); }, duration); 68 } 69 70 /* ââ Load chapter ââââââââââââââââââââââââââââââââââââââââââââââ */ 71 function loadChapter(index, startTime) { 72 var $item = $chapters.eq(index); 73 if (!$item.length) return; 74 var url = $item.data('url'); 75 if (!url) return; 76 77 currentChapter = index; 78 $chapters.removeClass('rap-active rap-playing'); 79 $item.addClass('rap-active'); 80 audio.src = url; 81 audio.load(); 82 83 if (startTime > 0) { 84 audio.addEventListener('loadedmetadata', function onMeta() { 85 audio.currentTime = startTime; 86 audio.removeEventListener('loadedmetadata', onMeta); 87 }); 88 } 89 } 90 91 /* ââ Play / Pause ââââââââââââââââââââââââââââââââââââââââââââââ */ 92 function setPlayState(playing) { 93 if (playing) { 94 $iconPlay.hide(); $iconPause.show(); 95 $chapters.eq(currentChapter).addClass('rap-playing'); 96 $playPause.attr('title', 'Pause'); 97 } else { 98 $iconPlay.show(); $iconPause.hide(); 99 $chapters.removeClass('rap-playing'); 100 $playPause.attr('title', 'Play'); 101 } 102 } 103 104 $playPause.on('click', function () { 105 if (audio.paused) { 106 if (!audio.getAttribute('src')) loadChapter(currentChapter, 0); 107 audio.play().catch(function (e) { console.warn('RAP play error:', e); }); 108 } else { 109 audio.pause(); 110 } 111 }); 112 113 $audio.on('play', function () { setPlayState(true); }); 114 $audio.on('pause', function () { setPlayState(false); }); 115 $audio.on('ended', function () { 116 var next = currentChapter + 1; 117 if ($chapters.eq(next).length) { loadChapter(next, 0); audio.play(); } 118 else { setPlayState(false); } 119 }); 120 121 /* ââ Progress bar ââââââââââââââââââââââââââââââââââââââââââââââ */ 122 $audio.on('timeupdate', function () { 123 if (isSeeking) return; 124 var pct = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0; 125 $fill.css('width', pct + '%'); 126 $thumb.css('left', pct + '%'); 127 $curTime.text(fmtTime(audio.currentTime)); 128 }); 129 130 $audio.on('durationchange', function () { 131 $totalTime.text(fmtTime(audio.duration)); 132 }); 133 134 $bar.on('mousedown touchstart', function (e) { isSeeking = true; seek(e); }); 135 $(document).on('mousemove touchmove', function (e) { if (isSeeking) seek(e); }) 136 .on('mouseup touchend', function () { if (!isSeeking) return; isSeeking = false; scheduleSave(); }); 137 138 function seek(e) { 139 var touch = e.originalEvent.touches ? e.originalEvent.touches[0] : e; 140 var rect = $bar[0].getBoundingClientRect(); 141 var ratio = Math.min(Math.max((touch.clientX - rect.left) / rect.width, 0), 1); 142 $fill.css('width', (ratio * 100) + '%'); 143 $thumb.css('left', (ratio * 100) + '%'); 144 if (audio.duration) audio.currentTime = ratio * audio.duration; 145 $curTime.text(fmtTime(audio.currentTime)); 146 } 147 148 /* ââ Skip buttons ââââââââââââââââââââââââââââââââââââââââââââââ */ 149 wrap.find('.rap-back').on('click', function () { audio.currentTime = Math.max(0, audio.currentTime - 10); }); 150 wrap.find('.rap-fwd').on('click', function () { audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 30); }); 151 wrap.find('.rap-prev').on('click', function () { 152 if (currentChapter > 0) { loadChapter(currentChapter - 1, 0); if (!audio.paused) audio.play(); } 153 }); 154 wrap.find('.rap-next').on('click', function () { 155 if (currentChapter < $chapters.length - 1) { loadChapter(currentChapter + 1, 0); if (!audio.paused) audio.play(); } 156 }); 157 158 /* ââ Chapter list clicks âââââââââââââââââââââââââââââââââââââââ */ 159 $chapters.on('click', function () { 160 loadChapter(parseInt($(this).data('index'), 10), 0); 161 audio.play(); 162 }); 163 164 /* ââ Speed & Volume ââââââââââââââââââââââââââââââââââââââââââââ */ 165 $speed.on('change', function () { audio.playbackRate = parseFloat($(this).val()); }); 166 $volume.on('input', function () { audio.volume = parseFloat($(this).val()); }); 167 168 /* ââ Chapter duration hints ââââââââââââââââââââââââââââââââââââ */ 169 $chapters.each(function () { 170 var $item = $(this); 171 var url = $item.data('url'); 172 if (!url) return; 173 var tmp = new Audio(); 174 tmp.preload = 'metadata'; 175 tmp.src = url; 176 tmp.addEventListener('loadedmetadata', function () { 177 $item.find('.rap-chapter-duration').text(fmtTime(tmp.duration)); 178 }); 179 }); 180 181 /* ââ Save bookmark âââââââââââââââââââââââââââââââââââââââââââââ */ 182 function scheduleSave() { 183 clearTimeout(saveTimer); 184 saveTimer = setTimeout(function () { 185 bmSave(bookId, currentChapter, audio.currentTime); 186 showStatus('Progress saved \u2713', 2000); 187 }, 1500); 188 } 189 190 // Save on pause 191 $audio.on('pause', scheduleSave); 192 193 // Save every 15 s while playing 194 var autoSaveInterval = setInterval(function () { 195 if (!audio.paused && audio.currentTime > 0) { 196 bmSave(bookId, currentChapter, audio.currentTime); 197 showStatus('Progress saved \u2713', 2000); 198 } 199 }, 15000); 200 201 // Save synchronously when page closes (localStorage is sync so this always works) 202 $(window).on('beforeunload', function () { 203 clearInterval(autoSaveInterval); 204 bmSave(bookId, currentChapter, audio.currentTime); 205 }); 206 207 /* ââ Restore bookmark on load ââââââââââââââââââââââââââââââââââ */ 208 loadChapter(0, 0); 209 210 var bm = bmGet(bookId); 211 if (bm && (bm.chapter_index > 0 || bm.timestamp >= 5)) { 212 loadChapter(bm.chapter_index || 0, parseFloat(bm.timestamp) || 0); 213 showStatus('Resuming from where you left off\u2026'); 214 } 215 } 216 217 /* ââ Boot ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 218 $(function () { 219 $('.rap-player').each(function () { initPlayer($(this)); }); 220 }); 221 222}(jQuery));
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.