
- Fixed double playback issue on stream page by properly scoping event delegation in streams-ui.js - Added init-personal-stream.js to handle UID for personal stream playback - Improved error handling and logging for audio playback - Added proper event propagation control to prevent duplicate event handling
943 lines
29 KiB
JavaScript
943 lines
29 KiB
JavaScript
// app.js — Frontend upload + minimal native player logic with slide-in and pulse effect
|
|
|
|
import { playBeep } from "./sound.js";
|
|
import { showToast } from "./toast.js";
|
|
import { injectNavigation } from "./inject-nav.js";
|
|
|
|
// Global audio state
|
|
let globalAudio = null;
|
|
let currentStreamUid = null;
|
|
let audioPlaying = false;
|
|
let lastPosition = 0;
|
|
|
|
// Utility functions
|
|
function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
return null;
|
|
}
|
|
|
|
// Log debug messages to server
|
|
export function logToServer(msg) {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("POST", "/log", true);
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
xhr.send(JSON.stringify({ msg }));
|
|
}
|
|
|
|
// Handle magic link login redirect
|
|
function handleMagicLoginRedirect() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('login') === 'success' && params.get('confirmed_uid')) {
|
|
const username = params.get('confirmed_uid');
|
|
console.log('Magic link login detected for user:', username);
|
|
|
|
// Update authentication state
|
|
localStorage.setItem('uid', username);
|
|
localStorage.setItem('confirmed_uid', username);
|
|
localStorage.setItem('uid_time', Date.now().toString());
|
|
document.cookie = `uid=${encodeURIComponent(username)}; path=/`;
|
|
|
|
// Update UI state
|
|
document.body.classList.add('authenticated');
|
|
document.body.classList.remove('guest');
|
|
|
|
// Update local storage and cookies
|
|
localStorage.setItem('isAuthenticated', 'true');
|
|
document.cookie = `isAuthenticated=true; path=/`;
|
|
|
|
// Update URL and history without reloading
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
// Update navigation
|
|
if (typeof injectNavigation === 'function') {
|
|
console.log('Updating navigation after magic link login');
|
|
injectNavigation(true);
|
|
} else {
|
|
console.warn('injectNavigation function not available after magic link login');
|
|
}
|
|
|
|
// Navigate to user's profile page
|
|
if (window.showOnly) {
|
|
console.log('Navigating to me-page');
|
|
window.showOnly('me-page');
|
|
} else if (window.location.hash !== '#me') {
|
|
window.location.hash = '#me';
|
|
}
|
|
|
|
// Auth state will be updated by the polling mechanism
|
|
}
|
|
}
|
|
|
|
// Audio player functions
|
|
function getOrCreateAudioElement() {
|
|
if (!globalAudio) {
|
|
globalAudio = document.getElementById('me-audio');
|
|
if (!globalAudio) {
|
|
console.error('Audio element not found');
|
|
return null;
|
|
}
|
|
|
|
globalAudio.preload = 'metadata';
|
|
globalAudio.crossOrigin = 'use-credentials';
|
|
globalAudio.setAttribute('crossorigin', 'use-credentials');
|
|
|
|
// Set up event listeners
|
|
globalAudio.addEventListener('play', () => {
|
|
audioPlaying = true;
|
|
updatePlayPauseButton();
|
|
});
|
|
|
|
globalAudio.addEventListener('pause', () => {
|
|
audioPlaying = false;
|
|
updatePlayPauseButton();
|
|
});
|
|
|
|
globalAudio.addEventListener('timeupdate', () => {
|
|
lastPosition = globalAudio.currentTime;
|
|
});
|
|
|
|
globalAudio.addEventListener('error', handleAudioError);
|
|
}
|
|
return globalAudio;
|
|
}
|
|
|
|
function handleAudioError(e) {
|
|
const error = this.error;
|
|
let errorMessage = 'Audio playback error';
|
|
let shouldShowToast = true;
|
|
|
|
if (error) {
|
|
switch(error.code) {
|
|
case MediaError.MEDIA_ERR_ABORTED:
|
|
errorMessage = 'Audio playback was aborted';
|
|
shouldShowToast = false; // Don't show toast for aborted operations
|
|
break;
|
|
case MediaError.MEDIA_ERR_NETWORK:
|
|
errorMessage = 'Network error while loading audio';
|
|
break;
|
|
case MediaError.MEDIA_ERR_DECODE:
|
|
errorMessage = 'Error decoding audio. The file may be corrupted.';
|
|
break;
|
|
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
// Don't show error for missing audio files on new accounts
|
|
if (this.currentSrc && this.currentSrc.includes('stream.opus')) {
|
|
console.log('Audio format not supported or file not found:', this.currentSrc);
|
|
return;
|
|
}
|
|
errorMessage = 'Audio format not supported. Please upload a supported format (Opus/OGG).';
|
|
break;
|
|
}
|
|
|
|
console.error('Audio error:', errorMessage, error);
|
|
|
|
// Only show error toast if we have a valid error and it's not a missing file
|
|
if (shouldShowToast && !(error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED && !this.src)) {
|
|
showToast(errorMessage, 'error');
|
|
}
|
|
}
|
|
|
|
console.error('Audio error:', {
|
|
error: error,
|
|
src: this.currentSrc,
|
|
networkState: this.networkState,
|
|
readyState: this.readyState
|
|
});
|
|
|
|
if (errorMessage !== 'Audio format not supported') {
|
|
showToast(`❌ ${errorMessage}`, 'error');
|
|
}
|
|
}
|
|
|
|
function updatePlayPauseButton(audio, button) {
|
|
if (button && audio) {
|
|
button.textContent = audio.paused ? '▶️' : '⏸️';
|
|
}
|
|
}
|
|
|
|
// Stream loading and playback
|
|
async function loadProfileStream(uid) {
|
|
const audio = getOrCreateAudioElement();
|
|
if (!audio) {
|
|
console.error('Failed to initialize audio element');
|
|
return null;
|
|
}
|
|
|
|
// Hide playlist controls
|
|
const mePrevBtn = document.getElementById("me-prev");
|
|
const meNextBtn = document.getElementById("me-next");
|
|
if (mePrevBtn) mePrevBtn.style.display = "none";
|
|
if (meNextBtn) meNextBtn.style.display = "none";
|
|
|
|
// Reset current stream and update audio source
|
|
currentStreamUid = uid;
|
|
audio.pause();
|
|
audio.removeAttribute('src');
|
|
audio.load();
|
|
|
|
// Wait a moment to ensure the previous source is cleared
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
const username = localStorage.getItem('username') || uid;
|
|
const audioUrl = `/audio/${encodeURIComponent(username)}/stream.opus?t=${Date.now()}`;
|
|
|
|
try {
|
|
console.log('Checking audio file at:', audioUrl);
|
|
|
|
// First check if the audio file exists and get its content type
|
|
const response = await fetch(audioUrl, {
|
|
method: 'HEAD',
|
|
headers: {
|
|
'Cache-Control': 'no-cache',
|
|
'Pragma': 'no-cache'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.log('No audio file found for user:', username);
|
|
updatePlayPauseButton(audio, document.querySelector('.play-pause-btn'));
|
|
return null;
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
console.log('Audio content type:', contentType);
|
|
|
|
if (!contentType || !contentType.includes('audio/')) {
|
|
throw new Error(`Invalid content type: ${contentType || 'unknown'}`);
|
|
}
|
|
|
|
// Set the audio source with proper type hint
|
|
const source = document.createElement('source');
|
|
source.src = audioUrl;
|
|
source.type = 'audio/ogg; codecs=opus';
|
|
|
|
// Clear any existing sources
|
|
while (audio.firstChild) {
|
|
audio.removeChild(audio.firstChild);
|
|
}
|
|
audio.appendChild(source);
|
|
|
|
// Load the new source
|
|
await new Promise((resolve, reject) => {
|
|
audio.load();
|
|
audio.oncanplaythrough = resolve;
|
|
audio.onerror = () => {
|
|
reject(new Error('Failed to load audio source'));
|
|
};
|
|
// Set a timeout in case the audio never loads
|
|
setTimeout(() => reject(new Error('Audio load timeout')), 10000);
|
|
});
|
|
|
|
console.log('Audio loaded, attempting to play...');
|
|
|
|
// Try to play immediately
|
|
try {
|
|
await audio.play();
|
|
audioPlaying = true;
|
|
console.log('Audio playback started successfully');
|
|
} catch (e) {
|
|
console.log('Auto-play failed, waiting for user interaction:', e);
|
|
audioPlaying = false;
|
|
// Don't show error for autoplay restrictions
|
|
if (!e.message.includes('play() failed because the user')) {
|
|
showToast('Click the play button to start playback', 'info');
|
|
}
|
|
}
|
|
|
|
// Show stream info if available
|
|
const streamInfo = document.getElementById("stream-info");
|
|
if (streamInfo) streamInfo.hidden = false;
|
|
|
|
} catch (error) {
|
|
console.error('Error checking/loading audio:', error);
|
|
// Don't show error toasts for missing audio files or aborted requests
|
|
if (error.name !== 'AbortError' &&
|
|
!error.message.includes('404') &&
|
|
!error.message.includes('Failed to load')) {
|
|
showToast('Error loading audio: ' + (error.message || 'Unknown error'), 'error');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Update button state
|
|
updatePlayPauseButton(audio, document.querySelector('.play-pause-btn'));
|
|
return audio;
|
|
}
|
|
|
|
// Navigation and UI functions
|
|
function showProfilePlayerFromUrl() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const profileUid = params.get("profile");
|
|
|
|
if (profileUid) {
|
|
const mePage = document.getElementById("me-page");
|
|
if (!mePage) return;
|
|
|
|
document.querySelectorAll("main > section").forEach(sec =>
|
|
sec.hidden = sec.id !== "me-page"
|
|
);
|
|
|
|
// Hide upload/delete/copy-url controls for guest view
|
|
const uploadArea = document.getElementById("upload-area");
|
|
if (uploadArea) uploadArea.hidden = true;
|
|
|
|
const copyUrlBtn = document.getElementById("copy-url");
|
|
if (copyUrlBtn) copyUrlBtn.style.display = "none";
|
|
|
|
const deleteBtn = document.getElementById("delete-account");
|
|
if (deleteBtn) deleteBtn.style.display = "none";
|
|
|
|
// Update UI for guest view
|
|
const meHeading = document.querySelector("#me-page h2");
|
|
if (meHeading) meHeading.textContent = `${profileUid}'s Stream 🎙️`;
|
|
|
|
const meDesc = document.querySelector("#me-page p");
|
|
if (meDesc) meDesc.textContent = `This is ${profileUid}'s public stream.`;
|
|
|
|
// Show a Play Stream button for explicit user action
|
|
const streamInfo = document.getElementById("stream-info");
|
|
if (streamInfo) {
|
|
streamInfo.innerHTML = '';
|
|
const playBtn = document.createElement('button');
|
|
playBtn.textContent = "▶ Play Stream";
|
|
playBtn.onclick = () => {
|
|
loadProfileStream(profileUid);
|
|
playBtn.disabled = true;
|
|
};
|
|
streamInfo.appendChild(playBtn);
|
|
streamInfo.hidden = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function initNavigation() {
|
|
// Get all navigation links
|
|
const navLinks = document.querySelectorAll('nav a, .dashboard-nav a');
|
|
|
|
// Handle navigation link clicks
|
|
const handleNavClick = (e) => {
|
|
const link = e.target.closest('a');
|
|
if (!link) return;
|
|
|
|
// Check both href and data-target attributes
|
|
const target = link.getAttribute('data-target');
|
|
const href = link.getAttribute('href');
|
|
|
|
// Let the browser handle external links
|
|
if (href && (href.startsWith('http') || href.startsWith('mailto:'))) {
|
|
return;
|
|
}
|
|
|
|
// If no data-target and no hash in href, let browser handle it
|
|
if (!target && (!href || !href.startsWith('#'))) {
|
|
return;
|
|
}
|
|
|
|
// Prefer data-target over href
|
|
let sectionId = target || (href ? href.substring(1) : '');
|
|
|
|
// Special case for the 'me' route which maps to 'me-page'
|
|
if (sectionId === 'me') {
|
|
sectionId = 'me-page';
|
|
}
|
|
|
|
// Skip if no valid section ID
|
|
if (!sectionId) {
|
|
console.warn('No valid section ID in navigation link:', link);
|
|
return;
|
|
}
|
|
|
|
// Let the router handle the navigation
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Update body class to reflect the current section
|
|
document.body.className = ''; // Clear existing classes
|
|
document.body.classList.add(`page-${sectionId.replace('-page', '')}`);
|
|
|
|
// Update URL hash - the router will handle the rest
|
|
window.location.hash = sectionId;
|
|
|
|
// Close mobile menu if open
|
|
const burger = document.getElementById('burger-toggle');
|
|
if (burger && burger.checked) burger.checked = false;
|
|
};
|
|
|
|
// Add click event listeners to all navigation links
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', handleNavClick);
|
|
});
|
|
|
|
// Handle initial page load and hash changes
|
|
const handleHashChange = () => {
|
|
let hash = window.location.hash.substring(1);
|
|
|
|
// Map URL hashes to section IDs if they don't match exactly
|
|
const sectionMap = {
|
|
'welcome': 'welcome-page',
|
|
'streams': 'stream-page',
|
|
'account': 'register-page',
|
|
'login': 'login-page',
|
|
'me': 'me-page'
|
|
};
|
|
|
|
// Use mapped section ID or the hash as is
|
|
const sectionId = sectionMap[hash] || hash || 'welcome-page';
|
|
const targetSection = document.getElementById(sectionId);
|
|
|
|
if (targetSection) {
|
|
// Hide all sections
|
|
document.querySelectorAll('main > section').forEach(section => {
|
|
section.hidden = section.id !== sectionId;
|
|
});
|
|
|
|
// Show target section
|
|
targetSection.hidden = false;
|
|
targetSection.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
// Update active state of navigation links
|
|
navLinks.forEach(link => {
|
|
const linkHref = link.getAttribute('href');
|
|
// Match both the exact hash and the mapped section ID
|
|
link.classList.toggle('active',
|
|
linkHref === `#${hash}` ||
|
|
linkHref === `#${sectionId}` ||
|
|
link.getAttribute('data-target') === sectionId ||
|
|
link.getAttribute('data-target') === hash
|
|
);
|
|
});
|
|
|
|
// Special handling for streams page
|
|
if (sectionId === 'stream-page' && typeof window.maybeLoadStreamsOnShow === 'function') {
|
|
window.maybeLoadStreamsOnShow();
|
|
}
|
|
} else {
|
|
console.warn(`Section with ID '${sectionId}' not found`);
|
|
}
|
|
};
|
|
|
|
// Listen for hash changes
|
|
window.addEventListener('hashchange', handleHashChange);
|
|
|
|
// Handle initial page load
|
|
handleHashChange();
|
|
}
|
|
|
|
function initProfilePlayer() {
|
|
if (typeof checkSessionValidity === "function" && !checkSessionValidity()) return;
|
|
showProfilePlayerFromUrl();
|
|
}
|
|
|
|
// Track previous authentication state
|
|
let wasAuthenticated = null;
|
|
// Debug flag - set to false to disable auth state change logs
|
|
const DEBUG_AUTH_STATE = false;
|
|
|
|
// Track all intervals and timeouts
|
|
const activeIntervals = new Map();
|
|
const activeTimeouts = new Map();
|
|
|
|
// Store original timer functions
|
|
const originalSetInterval = window.setInterval;
|
|
const originalClearInterval = window.clearInterval;
|
|
const originalSetTimeout = window.setTimeout;
|
|
const originalClearTimeout = window.clearTimeout;
|
|
|
|
// Override setInterval to track all intervals
|
|
window.setInterval = (callback, delay, ...args) => {
|
|
const id = originalSetInterval((...args) => {
|
|
trackFunctionCall('setInterval callback', { id, delay, callback: callback.toString() });
|
|
return callback(...args);
|
|
}, delay, ...args);
|
|
|
|
activeIntervals.set(id, {
|
|
id,
|
|
delay,
|
|
callback: callback.toString(),
|
|
createdAt: Date.now(),
|
|
stack: new Error().stack
|
|
});
|
|
|
|
if (DEBUG_AUTH_STATE) {
|
|
console.log(`[Interval ${id}] Created with delay ${delay}ms`, {
|
|
id,
|
|
delay,
|
|
callback: callback.toString(),
|
|
stack: new Error().stack
|
|
});
|
|
}
|
|
|
|
return id;
|
|
};
|
|
|
|
// Override clearInterval to track interval cleanup
|
|
window.clearInterval = (id) => {
|
|
if (activeIntervals.has(id)) {
|
|
activeIntervals.delete(id);
|
|
if (DEBUG_AUTH_STATE) {
|
|
console.log(`[Interval ${id}] Cleared`);
|
|
}
|
|
} else if (DEBUG_AUTH_STATE) {
|
|
console.log(`[Interval ${id}] Cleared (not tracked)`);
|
|
}
|
|
originalClearInterval(id);
|
|
};
|
|
|
|
// Override setTimeout to track timeouts (debug logging disabled)
|
|
window.setTimeout = (callback, delay, ...args) => {
|
|
const id = originalSetTimeout(callback, delay, ...args);
|
|
// Store minimal info without logging
|
|
activeTimeouts.set(id, {
|
|
id,
|
|
delay,
|
|
callback: callback.toString(),
|
|
createdAt: Date.now()
|
|
});
|
|
return id;
|
|
};
|
|
|
|
// Override clearTimeout to track timeout cleanup (debug logging disabled)
|
|
window.clearTimeout = (id) => {
|
|
if (activeTimeouts.has(id)) {
|
|
activeTimeouts.delete(id);
|
|
}
|
|
originalClearTimeout(id);
|
|
};
|
|
|
|
// Track auth check calls
|
|
let lastAuthCheckTime = 0;
|
|
let authCheckCounter = 0;
|
|
const AUTH_CHECK_DEBOUNCE = 1000; // 1 second
|
|
|
|
// Override console.log to capture all logs
|
|
const originalConsoleLog = console.log;
|
|
const originalConsoleGroup = console.group;
|
|
const originalConsoleGroupEnd = console.groupEnd;
|
|
|
|
// Track all console logs
|
|
const consoleLogs = [];
|
|
const MAX_LOGS = 100;
|
|
|
|
console.log = function(...args) {
|
|
// Store the log
|
|
consoleLogs.push({
|
|
type: 'log',
|
|
timestamp: new Date().toISOString(),
|
|
args: [...args],
|
|
stack: new Error().stack
|
|
});
|
|
|
|
// Keep only the most recent logs
|
|
while (consoleLogs.length > MAX_LOGS) {
|
|
consoleLogs.shift();
|
|
}
|
|
|
|
// Filter out the auth state check messages
|
|
if (args[0] && typeof args[0] === 'string' && args[0].includes('Auth State Check')) {
|
|
return;
|
|
}
|
|
|
|
originalConsoleLog.apply(console, args);
|
|
};
|
|
|
|
// Track console groups
|
|
console.group = function(...args) {
|
|
consoleLogs.push({ type: 'group', timestamp: new Date().toISOString(), args });
|
|
originalConsoleGroup.apply(console, args);
|
|
};
|
|
|
|
console.groupEnd = function() {
|
|
consoleLogs.push({ type: 'groupEnd', timestamp: new Date().toISOString() });
|
|
originalConsoleGroupEnd.apply(console);
|
|
};
|
|
|
|
// Track all function calls that might trigger auth checks
|
|
const trackedFunctions = ['checkAuthState', 'setInterval', 'setTimeout', 'addEventListener', 'removeEventListener'];
|
|
const functionCalls = [];
|
|
const MAX_FUNCTION_CALLS = 100;
|
|
|
|
|
|
|
|
// Function to format stack trace for better readability
|
|
function formatStack(stack) {
|
|
if (!stack) return 'No stack trace';
|
|
// Remove the first line (the error message) and limit to 5 frames
|
|
const frames = stack.split('\n').slice(1).slice(0, 5);
|
|
return frames.join('\n');
|
|
}
|
|
|
|
// Function to dump debug info
|
|
window.dumpDebugInfo = () => {
|
|
console.group('Debug Info');
|
|
|
|
// Active Intervals
|
|
console.group('Active Intervals');
|
|
if (activeIntervals.size === 0) {
|
|
console.log('No active intervals');
|
|
} else {
|
|
activeIntervals.forEach((info, id) => {
|
|
console.group(`Interval ${id} (${info.delay}ms)`);
|
|
console.log('Created at:', new Date(info.createdAt).toISOString());
|
|
console.log('Callback:', info.callback.split('\n')[0]); // First line of callback
|
|
console.log('Stack trace:');
|
|
console.log(formatStack(info.stack));
|
|
console.groupEnd();
|
|
});
|
|
}
|
|
console.groupEnd();
|
|
|
|
// Active Timeouts
|
|
console.group('Active Timeouts');
|
|
if (activeTimeouts.size === 0) {
|
|
console.log('No active timeouts');
|
|
} else {
|
|
activeTimeouts.forEach((info, id) => {
|
|
console.group(`Timeout ${id} (${info.delay}ms)`);
|
|
console.log('Created at:', new Date(info.createdAt).toISOString());
|
|
console.log('Callback:', info.callback.split('\n')[0]); // First line of callback
|
|
console.log('Stack trace:');
|
|
console.log(formatStack(info.stack));
|
|
console.groupEnd();
|
|
});
|
|
}
|
|
console.groupEnd();
|
|
|
|
// Document state
|
|
console.group('Document State');
|
|
console.log('Visibility:', document.visibilityState);
|
|
console.log('Has Focus:', document.hasFocus());
|
|
console.log('URL:', window.location.href);
|
|
console.groupEnd();
|
|
|
|
// Recent logs
|
|
console.group('Recent Logs (10 most recent)');
|
|
if (consoleLogs.length === 0) {
|
|
console.log('No logs recorded');
|
|
} else {
|
|
consoleLogs.slice(-10).forEach((log, i) => {
|
|
console.group(`Log ${i + 1} (${log.timestamp})`);
|
|
console.log(...log.args);
|
|
if (log.stack) {
|
|
console.log('Stack trace:');
|
|
console.log(formatStack(log.stack));
|
|
}
|
|
console.groupEnd();
|
|
});
|
|
}
|
|
console.groupEnd();
|
|
|
|
// Auth state
|
|
console.group('Auth State');
|
|
console.log('Has auth cookie:', document.cookie.includes('sessionid='));
|
|
console.log('Has UID cookie:', document.cookie.includes('uid='));
|
|
console.log('Has localStorage auth:', localStorage.getItem('isAuthenticated') === 'true');
|
|
console.log('Has auth token:', !!localStorage.getItem('auth_token'));
|
|
console.groupEnd();
|
|
|
|
console.groupEnd(); // End main group
|
|
};
|
|
|
|
function trackFunctionCall(name, ...args) {
|
|
const callInfo = {
|
|
name,
|
|
time: Date.now(),
|
|
timestamp: new Date().toISOString(),
|
|
args: args.map(arg => {
|
|
try {
|
|
return JSON.stringify(arg);
|
|
} catch (e) {
|
|
return String(arg);
|
|
}
|
|
}),
|
|
stack: new Error().stack
|
|
};
|
|
|
|
functionCalls.push(callInfo);
|
|
if (functionCalls.length > MAX_FUNCTION_CALLS) {
|
|
functionCalls.shift();
|
|
}
|
|
|
|
if (name === 'checkAuthState') {
|
|
console.group(`[${functionCalls.length}] Auth check at ${callInfo.timestamp}`);
|
|
console.log('Call stack:', callInfo.stack);
|
|
console.log('Recent function calls:', functionCalls.slice(-5));
|
|
console.groupEnd();
|
|
}
|
|
}
|
|
|
|
// Override tracked functions
|
|
trackedFunctions.forEach(fnName => {
|
|
if (window[fnName]) {
|
|
const originalFn = window[fnName];
|
|
window[fnName] = function(...args) {
|
|
trackFunctionCall(fnName, ...args);
|
|
return originalFn.apply(this, args);
|
|
};
|
|
}
|
|
});
|
|
|
|
// Check authentication state and update UI
|
|
function checkAuthState() {
|
|
const now = Date.now();
|
|
|
|
// Throttle the checks
|
|
if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE) {
|
|
return;
|
|
}
|
|
lastAuthCheckTime = now;
|
|
|
|
// Check various auth indicators
|
|
const hasAuthCookie = document.cookie.includes('sessionid=');
|
|
const hasUidCookie = document.cookie.includes('uid=');
|
|
const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true';
|
|
const hasAuthToken = localStorage.getItem('authToken') !== null;
|
|
|
|
const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken;
|
|
|
|
// Only log if debug is enabled or if state has changed
|
|
if (DEBUG_AUTH_STATE || isAuthenticated !== wasAuthenticated) {
|
|
console.log('Auth State Check:', {
|
|
hasAuthCookie,
|
|
hasUidCookie,
|
|
hasLocalStorageAuth,
|
|
hasAuthToken,
|
|
isAuthenticated,
|
|
wasAuthenticated
|
|
});
|
|
}
|
|
|
|
// Only update if authentication state has changed
|
|
if (isAuthenticated !== wasAuthenticated) {
|
|
if (DEBUG_AUTH_STATE) {
|
|
console.log('Auth state changed, updating navigation...');
|
|
}
|
|
|
|
// Update UI state
|
|
if (isAuthenticated) {
|
|
document.body.classList.add('authenticated');
|
|
document.body.classList.remove('guest');
|
|
} else {
|
|
document.body.classList.remove('authenticated');
|
|
document.body.classList.add('guest');
|
|
}
|
|
|
|
// Update navigation
|
|
if (typeof injectNavigation === 'function') {
|
|
injectNavigation(isAuthenticated);
|
|
} else if (DEBUG_AUTH_STATE) {
|
|
console.warn('injectNavigation function not found');
|
|
}
|
|
|
|
// Update the tracked state
|
|
wasAuthenticated = isAuthenticated;
|
|
|
|
// Force reflow to ensure CSS updates
|
|
void document.body.offsetHeight;
|
|
}
|
|
|
|
return isAuthenticated;
|
|
}
|
|
|
|
// Periodically check authentication state
|
|
function setupAuthStatePolling() {
|
|
// Initial check
|
|
checkAuthState();
|
|
|
|
// Check every 30 seconds instead of 2 to reduce load
|
|
setInterval(checkAuthState, 30000);
|
|
|
|
// Also check after certain events that might affect auth state
|
|
window.addEventListener('storage', checkAuthState);
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden) checkAuthState();
|
|
});
|
|
}
|
|
|
|
|
|
// Initialize the application when DOM is loaded
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Set up authentication state monitoring
|
|
setupAuthStatePolling();
|
|
|
|
// Handle magic link redirect if needed
|
|
handleMagicLoginRedirect();
|
|
|
|
// Initialize components
|
|
initNavigation();
|
|
|
|
|
|
// Initialize profile player after a short delay
|
|
setTimeout(() => {
|
|
initProfilePlayer();
|
|
|
|
// Set up play/pause button click handler
|
|
document.addEventListener('click', (e) => {
|
|
const playPauseBtn = e.target.closest('.play-pause-btn');
|
|
if (!playPauseBtn || playPauseBtn.id === 'logout-button') return;
|
|
|
|
const audio = getOrCreateAudioElement();
|
|
if (!audio) return;
|
|
|
|
try {
|
|
if (audio.paused) {
|
|
// Stop any currently playing audio first
|
|
if (window.currentlyPlayingAudio && window.currentlyPlayingAudio !== audio) {
|
|
window.currentlyPlayingAudio.pause();
|
|
if (window.currentlyPlayingButton) {
|
|
updatePlayPauseButton(window.currentlyPlayingAudio, window.currentlyPlayingButton);
|
|
}
|
|
}
|
|
|
|
// Stop any playing public streams
|
|
const publicPlayers = document.querySelectorAll('.stream-player audio');
|
|
publicPlayers.forEach(player => {
|
|
if (!player.paused) {
|
|
player.pause();
|
|
const btn = player.closest('.stream-player').querySelector('.play-pause-btn');
|
|
if (btn) updatePlayPauseButton(player, btn);
|
|
}
|
|
});
|
|
|
|
// Check if audio has a valid source before attempting to play
|
|
// Only show this message for the main player, not public streams
|
|
if (!audio.src && !playPauseBtn.closest('.stream-player')) {
|
|
console.log('No audio source available for main player');
|
|
showToast('No audio file available. Please upload an audio file first.', 'info');
|
|
audioPlaying = false;
|
|
updatePlayPauseButton(audio, playPauseBtn);
|
|
return;
|
|
}
|
|
|
|
// Store the current play promise to handle aborts
|
|
const playPromise = audio.play();
|
|
|
|
// Handle successful play
|
|
playPromise.then(() => {
|
|
// Only update state if this is still the current play action
|
|
if (audio === getMainAudio()) {
|
|
window.currentlyPlayingAudio = audio;
|
|
window.currentlyPlayingButton = playPauseBtn;
|
|
updatePlayPauseButton(audio, playPauseBtn);
|
|
}
|
|
}).catch(e => {
|
|
// Don't log aborted errors as they're normal during rapid play/pause
|
|
if (e.name !== 'AbortError') {
|
|
console.error('Play failed:', e);
|
|
} else {
|
|
console.log('Playback was aborted as expected');
|
|
return; // Skip UI updates for aborted play
|
|
}
|
|
|
|
// Only update state if this is still the current audio element
|
|
if (audio === getMainAudio()) {
|
|
audioPlaying = false;
|
|
updatePlayPauseButton(audio, playPauseBtn);
|
|
|
|
// Provide more specific error messages
|
|
if (e.name === 'NotSupportedError' || e.name === 'NotAllowedError') {
|
|
showToast('Could not play audio. The format may not be supported.', 'error');
|
|
} else if (e.name !== 'AbortError') { // Skip toast for aborted errors
|
|
showToast('Failed to play audio. Please try again.', 'error');
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
audio.pause();
|
|
if (window.currentlyPlayingAudio === audio) {
|
|
window.currentlyPlayingAudio = null;
|
|
window.currentlyPlayingButton = null;
|
|
}
|
|
updatePlayPauseButton(audio, playPauseBtn);
|
|
}
|
|
} catch (e) {
|
|
console.error('Audio error:', e);
|
|
updatePlayPauseButton(audio, playPauseBtn);
|
|
}
|
|
});
|
|
|
|
// Set up delete account button if it exists
|
|
const deleteAccountBtn = document.getElementById('delete-account');
|
|
const deleteAccountFromPrivacyBtn = document.getElementById('delete-account-from-privacy');
|
|
|
|
const deleteAccount = async (e) => {
|
|
if (e) e.preventDefault();
|
|
|
|
if (!confirm('Are you sure you want to delete your account? This action cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/delete-account', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Clear local storage and redirect to home page
|
|
localStorage.clear();
|
|
window.location.href = '/';
|
|
} else {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to delete account');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting account:', error);
|
|
showToast(`❌ ${error.message || 'Failed to delete account'}`, 'error');
|
|
}
|
|
};
|
|
|
|
// Add event listeners to both delete account buttons
|
|
if (deleteAccountBtn) {
|
|
deleteAccountBtn.addEventListener('click', deleteAccount);
|
|
}
|
|
|
|
if (deleteAccountFromPrivacyBtn) {
|
|
deleteAccountFromPrivacyBtn.addEventListener('click', deleteAccount);
|
|
}
|
|
|
|
}, 200); // End of setTimeout
|
|
});
|
|
|
|
// Logout function
|
|
function logout() {
|
|
// Clear authentication state
|
|
document.body.classList.remove('authenticated');
|
|
localStorage.removeItem('isAuthenticated');
|
|
localStorage.removeItem('uid');
|
|
localStorage.removeItem('confirmed_uid');
|
|
localStorage.removeItem('uid_time');
|
|
|
|
// Clear cookies
|
|
document.cookie = 'isAuthenticated=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
|
document.cookie = 'uid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
|
|
|
// Stop any playing audio
|
|
stopMainAudio();
|
|
|
|
// Redirect to home page
|
|
window.location.href = '/';
|
|
}
|
|
|
|
// Add click handler for logout button
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.id === 'logout-button' || e.target.closest('#logout-button')) {
|
|
e.preventDefault();
|
|
logout();
|
|
}
|
|
});
|
|
|
|
// Expose functions for global access
|
|
window.logToServer = logToServer;
|
|
window.getMainAudio = () => globalAudio;
|
|
window.stopMainAudio = () => {
|
|
if (globalAudio) {
|
|
globalAudio.pause();
|
|
audioPlaying = false;
|
|
updatePlayPauseButton();
|
|
}
|
|
};
|
|
window.loadProfileStream = loadProfileStream;
|