PageSourceSearch

https://downround.net/app.js

js downround.net collected 2026-10-05 07:09:48 UTC 13,972 bytes, 418 lines download raw bytes

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.