1/* âââââââââââââââââââââââââââââââââââââââ 2 DOWN ROUND â APP LOGIC 3 âââââââââââââââââââââââââââââââââââââââ */ 4 5const RSS_PROXY_URL = '/api/rss'; 6const RSS_DIRECT_URL = 'https://www.omnycontent.com/d/playlist/d3d56d8d-11c9-411a-aade-af8f001be4a7/e6042ec6-f457-4ae1-9758-b0420054fb68/3a041c8f-14fe-4c99-a4d7-b0420054fb8d/podcast.rss'; 7const PREMIUM_URL = 'https://downround.memberful.com/join?splash=1'; 8 9// ââ Elements ââ 10const callScreen = document.getElementById('call-screen'); 11const playerScreen = document.getElementById('player-screen'); 12const popupsContainer = document.getElementById('popups-container'); 13const audio = document.getElementById('podcast-audio'); 14 15const btnAnswer = document.getElementById('btn-answer'); 16const btnDecline = document.getElementById('btn-decline'); 17const btnPlayPause = document.getElementById('btn-play-pause'); 18const btnRewind = document.getElementById('btn-rewind'); 19const btnForward = document.getElementById('btn-forward'); 20const btnHangup = document.getElementById('btn-hangup'); 21 22const iconPlay = document.getElementById('icon-play'); 23const iconPause = document.getElementById('icon-pause'); 24 25const progressBar = document.getElementById('progress-bar'); 26const progressFill = document.getElementById('progress-fill'); 27const progressKnob = document.getElementById('progress-knob'); 28const timeCurrent = document.getElementById('time-current'); 29const timeRemaining = document.getElementById('time-remaining'); 30 31const volumeBar = document.getElementById('volume-bar'); 32const volumeFill = document.getElementById('volume-fill'); 33const volumeKnob = document.getElementById('volume-knob'); 34 35const episodeTitle = document.getElementById('episode-title'); 36const statusTime = document.getElementById('status-time'); 37 38// ââ State ââ 39let latestEpisode = null; 40let popupInterval = null; 41let ringtoneOsc = null; 42let ringtoneCtx = null; 43 44// ââ Clock ââ 45function updateClock() { 46 const now = new Date(); 47 const h = now.getHours(); 48 const m = now.getMinutes().toString().padStart(2, '0'); 49 statusTime.textContent = `${h}:${m}`; 50} 51updateClock(); 52setInterval(updateClock, 10000); 53 54// ââ Fetch latest episode from RSS ââ 55async function fetchRSS(url) { 56 const resp = await fetch(url); 57 if (!resp.ok) throw new Error(`HTTP ${resp.status}`); 58 const text = await resp.text(); 59 const parser = new DOMParser(); 60 const xml = parser.parseFromString(text, 'application/xml'); 61 const items = xml.querySelectorAll('item'); 62 if (items.length === 0) throw new Error('No items in feed'); 63 64 const item = items[0]; 65 const title = item.querySelector('title')?.textContent || 'Down Round'; 66 const enclosure = item.querySelector('enclosure'); 67 const audioUrl = enclosure?.getAttribute('url') || ''; 68 69 return { 70 title: title, 71 audioUrl: audioUrl.replace(/&/g, '&'), 72 }; 73} 74 75async function fetchLatestEpisode() { 76 // Try proxy first (works on Cloudflare Pages), then direct, then fallback 77 for (const url of [RSS_PROXY_URL, RSS_DIRECT_URL]) { 78 try { 79 latestEpisode = await fetchRSS(url); 80 episodeTitle.textContent = latestEpisode.title; 81 return; 82 } catch (e) { 83 console.warn(`RSS fetch failed for ${url}:`, e.message); 84 } 85 } 86 87 // Hardcoded fallback 88 console.warn('All RSS fetches failed, using hardcoded fallback'); 89 latestEpisode = { 90 title: 'Tim Cook Is Out', 91 audioUrl: 'https://traffic.omny.fm/d/clips/d3d56d8d-11c9-411a-aade-af8f001be4a7/e6042ec6-f457-4ae1-9758-b0420054fb68/e2d1db77-f475-4b22-a49e-b43400827a4f/audio.mp3?utm_source=Podcast&in_playlist=3a041c8f-14fe-4c99-a4d7-b0420054fb8d', 92 }; 93 episodeTitle.textContent = latestEpisode.title; 94} 95 96fetchLatestEpisode(); 97 98// ââ Ringtone (Web Audio API) ââ 99function startRingtone() { 100 try { 101 ringtoneCtx = new (window.AudioContext || window.webkitAudioContext)(); 102 const playRing = () => { 103 if (!ringtoneCtx || ringtoneCtx.state === 'closed') return; 104 105 // Two-tone ring like a phone 106 const now = ringtoneCtx.currentTime; 107 108 for (let burst = 0; burst < 2; burst++) { 109 const osc = ringtoneCtx.createOscillator(); 110 const gain = ringtoneCtx.createGain(); 111 osc.connect(gain); 112 gain.connect(ringtoneCtx.destination); 113 osc.type = 'sine'; 114 osc.frequency.value = burst === 0 ? 440 : 480; 115 gain.gain.setValueAtTime(0.08, now + burst * 0.6); 116 gain.gain.exponentialRampToValueAtTime(0.001, now + burst * 0.6 + 0.5); 117 osc.start(now + burst * 0.6); 118 osc.stop(now + burst * 0.6 + 0.5); 119 } 120 }; 121 122 playRing(); 123 ringtoneOsc = setInterval(playRing, 3000); 124 } catch(e) { 125 // Audio context not available 126 } 127} 128 129// Answer Call ââ 130function answerCall() { 131 stopRingtone(); 132 callScreen.classList.remove('active'); 133 134 setTimeout(() => { 135 playerScreen.classList.add('active'); 136 if (latestEpisode && latestEpisode.audioUrl) { 137 audio.src = latestEpisode.audioUrl; 138 audio.play().catch(() => {}); 139 showPlayIcon(false); 140 } 141 }, 300); 142} 143 144// Decline / Hangup ââ 145function hangUp() { 146 stopRingtone(); 147 audio.pause(); 148 audio.currentTime = 0; 149 showPlayIcon(true); 150 151 callScreen.classList.remove('active'); 152 playerScreen.classList.remove('active'); 153 154 // Unleash the popups 155 popupsContainer.classList.add('active'); 156 spawnPopups(); 157} 158 159// Popup Storm ââ 160const ERROR_MESSAGES = [ 161 'Subscribe to Down Round Premium for ad-free listening and an extra episode every week!', 162 'WARNING: You are not a Premium subscriber. This is a critical error.', 163 'Your free listening credits have expired. Subscribe to Premium to continue.', 164 'FATAL ERROR: Insufficient Premium detected in your system.', 165 'Down Round Premium required. Click OK to resolve this issue.', 166 'System alert: James and Raph demand your subscription.', 167 'ERROR 402: Payment Required. Subscribe to Premium.', 168 'A Premium subscription is required to close this window.', 169 'Uh oh! It looks like you hung up on James and Raph. Subscribe to make it right.', 170 'This program has performed an illegal operation: Not subscribing to Premium.', 171]; 172 173let popupCount = 0; 174 175function spawnPopups() { 176 popupCount = 0; 177 // Initial burst 178 for (let i = 0; i < 3; i++) { 179 setTimeout(() => createPopup(), i * 200); 180 } 181 // Keep spawning 182 popupInterval = setInterval(() => { 183 if (popupCount < 15) { 184 createPopup(); 185 } else { 186 clearInterval(popupInterval); 187 } 188 }, 800); 189} 190 191function createPopup() { 192 popupCount++; 193 const popup = document.createElement('div'); 194 popup.className = 'win-error'; 195 196 const screenRect = document.getElementById('phone-screen').getBoundingClientRect(); 197 const phoneScreen = document.getElementById('phone-screen'); 198 const maxX = phoneScreen.clientWidth - 240; 199 const maxY = phoneScreen.clientHeight - 180; 200 201 popup.style.left = Math.max(5, Math.random() * maxX) + 'px'; 202 popup.style.top = Math.max(50, Math.random() * maxY) + 'px'; 203 204 const msg = ERROR_MESSAGES[Math.floor(Math.random() * ERROR_MESSAGES.length)]; 205 206 popup.innerHTML = ` 207 <div class="win-error-titlebar">
208 <span>Down Round Premium</span> 209 <div class="win-error-close" onclick="this.closest('.win-error').remove(); createPopup();">â</div> 210 </div> 211 <div class="win-error-body"> 212 <div class="error-content"> 213 <div class="win-error-icon">â ï¸</div> 214 <div class="win-error-message">${msg}</div> 215 </div> 216 <button class="win-error-ok" onclick="window.open('${PREMIUM_URL}', '_blank')">OK</button> 217 </div> 218 `; 219 220 // Make popup draggable 221 makeDraggable(popup); 222 223 popupsContainer.appendChild(popup); 224} 225 226function makeDraggable(el) { 227 const titlebar = el.querySelector('.win-error-titlebar'); 228 let isDragging = false; 229 let startX, startY, origLeft, origTop; 230 231 const onStart = (e) => { 232 if (e.target.classList.contains('win-error-close')) return; 233 isDragging = true; 234 const ev = e.touches ? e.touches[0] : e; 235 startX = ev.clientX; 236 startY = ev.clientY; 237 238 const rect = el.getBoundingClientRect(); 239 const parentRect = el.parentElement.getBoundingClientRect(); 240 origLeft = rect.left - parentRect.left; 241 origTop = rect.top - parentRect.top; 242 243 el.style.zIndex = 310 + popupCount; 244 e.preventDefault(); 245 }; 246 247 const onMove = (e) => { 248 if (!isDragging) return; 249 const ev = e.touches ? e.touches[0] : e; 250 const dx = ev.clientX - startX; 251 const dy = ev.clientY - startY; 252 el.style.left = (origLeft + dx) + 'px'; 253 el.style.top = (origTop + dy) + 'px'; 254 e.preventDefault(); 255 }; 256 257 const onEnd = () => { 258 isDragging = false; 259 }; 260 261 titlebar.addEventListener('mousedown', onStart); 262 document.addEventListener('mousemove', onMove); 263 document.addEventListener('mouseup', onEnd); 264 265 titlebar.addEventListener('touchstart', onStart, { passive: false }); 266 document.addEventListener('touchmove', onMove, { passive: false }); 267 document.addEventListener('touchend', onEnd); 268} 269 270// Play/Pause ââ 271function showPlayIcon(showPlay) { 272 iconPlay.style.display = showPlay ? 'block' : 'none'; 273 iconPause.style.display = showPlay ? 'none' : 'block'; 274} 275 276function togglePlayPause() { 277 if (audio.paused) { 278 audio.play().catch(() => {}); 279 showPlayIcon(false); 280 } else { 281 audio.pause(); 282 showPlayIcon(true); 283 } 284} 285 286// Time Formatting ââ 287function formatTime(seconds) { 288 if (isNaN(seconds) || !isFinite(seconds)) return '0:00'; 289 const m = Math.floor(seconds / 60); 290 const s = Math.floor(seconds % 60).toString().padStart(2, '0'); 291 return `${m}:${s}`; 292} 293 294// Audio Progress ââ 295audio.addEventListener('timeupdate', () => { 296 if (!audio.duration) return; 297 const pct = (audio.currentTime / audio.duration) * 100; 298 progressFill.style.width = pct + '%'; 299 progressKnob.style.left = pct + '%'; 300 timeCurrent.textContent = formatTime(audio.currentTime); 301 timeRemaining.textContent = '-' + formatTime(audio.duration - audio.currentTime); 302}); 303 304audio.addEventListener('ended', () => { 305 showPlayIcon(true); 306}); 307 308// Progress Seeking ââ 309function seekFromEvent(e) { 310 const rect = progressBar.getBoundingClientRect(); 311 const ev = e.touches ? e.touches[0] : e; 312 let pct = (ev.clientX - rect.left) / rect.width; 313 pct = Math.max(0, Math.min(1, pct)); 314 if (audio.duration) { 315 audio.currentTime = pct * audio.duration; 316 } 317} 318 319progressBar.addEventListener('click', seekFromEvent); 320 321let seekingProgress = false; 322progressBar.addEventListener('mousedown', (e) => { seekingProgress = true; seekFromEvent(e); }); 323document.addEventListener('mousemove', (e) => { if (seekingProgress) seekFromEvent(e); }); 324document.addEventListener('mouseup', () => { seekingProgress = false; }); 325progressBar.addEventListener('touchstart', (e) => { seekingProgress = true; seekFromEvent(e); }, { passive: true }); 326document.addEventListener('touchmove', (e) => { if (seekingProgress) seekFromEvent(e); }, { passive: true }); 327document.addEventListener('touchend', () => { seekingProgress = false; }); 328 329// Volume ââ 330function setVolumeFromEvent(e) { 331 const rect = volumeBar.getBoundingClientRect(); 332 const ev = e.touches ? e.touches[0] : e; 333 let pct = (ev.clientX - rect.left) / rect.width; 334 pct = Math.max(0, Math.min(1, pct)); 335 audio.volume = pct; 336 volumeFill.style.width = (pct * 100) + '%'; 337 volumeKnob.style.left = (pct * 100) + '%'; 338} 339 340volumeBar.addEventListener('click', setVolumeFromEvent); 341 342let seekingVolume = false;
343volumeBar.addEventListener('mousedown', (e) => { seekingVolume = true; setVolumeFromEvent(e); }); 344document.addEventListener('mousemove', (e) => { if (seekingVolume) setVolumeFromEvent(e); }); 345document.addEventListener('mouseup', () => { seekingVolume = false; }); 346volumeBar.addEventListener('touchstart', (e) => { seekingVolume = true; setVolumeFromEvent(e); }, { passive: true }); 347document.addEventListener('touchmove', (e) => { if (seekingVolume) setVolumeFromEvent(e); }, { passive: true }); 348document.addEventListener('touchend', () => { seekingVolume = false; }); 349 350// Button Events ââ 351btnAnswer.addEventListener('click', answerCall); 352btnDecline.addEventListener('click', hangUp); 353btnPlayPause.addEventListener('click', togglePlayPause); 354btnHangup.addEventListener('click', hangUp); 355 356btnRewind.addEventListener('click', () => { 357 audio.currentTime = Math.max(0, audio.currentTime - 15); 358}); 359 360btnForward.addEventListener('click', () => { 361 audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 30); 362}); 363 364// Start ringtone on first interaction (mobile audio policy) ââ 365let ringtoneStarted = false; 366document.addEventListener('click', () => { 367 if (!ringtoneStarted && callScreen.classList.contains('active')) { 368 startRingtone(); 369 ringtoneStarted = true; 370 } 371}, { once: true }); 372 373// Attempt auto-start ringtone (works on desktop mostly) 374setTimeout(() => { 375 if (!ringtoneStarted && callScreen.classList.contains('active')) { 376 startRingtone(); 377 ringtoneStarted = true; 378 } 379}, 500); 380 381// Bonzi Buddy speech rotation ââ 382const bonziMessages = [ 383 'Subscribe to Premium!', 384 'Ad-free = more time to grind', 385 'extra episode = 2x the alpha', 386 'Click the ape', 387 'escape the permanent underclass', 388 'i have a gun', 389 'click to subscribe', 390]; 391 392let bonziIdx = 0; 393setInterval(() => { 394 bonziIdx = (bonziIdx + 1) % bonziMessages.length; 395 const speech = document.querySelector('#bonzi-speech p'); 396 speech.style.opacity = '0'; 397 setTimeout(() => { 398 speech.textContent = bonziMessages[bonziIdx]; 399 speech.style.opacity = '1'; 400 }, 200); 401}, 4000); 402 403// Smooth opacity transition for bonzi speech 404document.querySelector('#bonzi-speech p').style.transition = 'opacity 0.2s ease'; 405 406// Stop Ringtone ââ 407function stopRingtone() { 408 if (ringtoneOsc) { 409 clearInterval(ringtoneOsc); 410 ringtoneOsc = null; 411 } 412 if (ringtoneCtx) { 413 ringtoneCtx.close().catch(() => {}); 414 ringtoneCtx = null; 415 } 416} 417 418
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.