feat: Add database migrations and auth system
- Add Alembic for database migrations - Implement user authentication system - Update frontend styles and components - Add new test audio functionality - Update stream management and UI
This commit is contained in:
790
static/app.js
790
static/app.js
@ -1,5 +1,15 @@
|
||||
// app.js — Frontend upload + minimal native player logic with slide-in and pulse effect
|
||||
|
||||
import { playBeep } from "./sound.js";
|
||||
import { showToast } from "./toast.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}=`);
|
||||
@ -7,10 +17,6 @@ function getCookie(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
import { playBeep } from "./sound.js";
|
||||
import { showToast } from "./toast.js";
|
||||
|
||||
|
||||
// Log debug messages to server
|
||||
export function logToServer(msg) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
@ -19,396 +25,476 @@ export function logToServer(msg) {
|
||||
xhr.send(JSON.stringify({ msg }));
|
||||
}
|
||||
|
||||
// Expose for debugging
|
||||
window.logToServer = logToServer;
|
||||
|
||||
// Handle magic link login redirect
|
||||
(function handleMagicLoginRedirect() {
|
||||
function handleMagicLoginRedirect() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('login') === 'success' && params.get('confirmed_uid')) {
|
||||
const username = params.get('confirmed_uid');
|
||||
localStorage.setItem('uid', username);
|
||||
logToServer(`[DEBUG] localStorage.setItem('uid', '${username}')`);
|
||||
localStorage.setItem('confirmed_uid', username);
|
||||
logToServer(`[DEBUG] localStorage.setItem('confirmed_uid', '${username}')`);
|
||||
const uidTime = Date.now().toString();
|
||||
localStorage.setItem('uid_time', uidTime);
|
||||
logToServer(`[DEBUG] localStorage.setItem('uid_time', '${uidTime}')`);
|
||||
// Set uid as cookie for backend authentication
|
||||
document.cookie = "uid=" + encodeURIComponent(username) + "; path=/";
|
||||
// Remove query params from URL
|
||||
localStorage.setItem('uid_time', Date.now().toString());
|
||||
document.cookie = `uid=${encodeURIComponent(username)}; path=/`;
|
||||
|
||||
// Update UI state immediately without reload
|
||||
const guestDashboard = document.getElementById('guest-dashboard');
|
||||
const userDashboard = document.getElementById('user-dashboard');
|
||||
const registerPage = document.getElementById('register-page');
|
||||
|
||||
if (guestDashboard) guestDashboard.style.display = 'none';
|
||||
if (userDashboard) userDashboard.style.display = 'block';
|
||||
if (registerPage) registerPage.style.display = 'none';
|
||||
|
||||
// Update URL and history without reloading
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
// Reload to show dashboard as logged in
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// (Removed duplicate logToServer definition)
|
||||
|
||||
// Guest vs. logged-in toggling is now handled by dashboard.js
|
||||
// --- Public profile view logic ---
|
||||
function showProfilePlayerFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const profileUid = params.get("profile");
|
||||
if (profileUid) {
|
||||
const mePage = document.getElementById("me-page");
|
||||
if (mePage) {
|
||||
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 heading and description 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;
|
||||
}
|
||||
// Do NOT call loadProfileStream(profileUid) automatically!
|
||||
}
|
||||
|
||||
// Navigate to user's profile page
|
||||
if (window.showOnly) {
|
||||
window.showOnly('me-page');
|
||||
} else if (window.location.hash !== '#me') {
|
||||
window.location.hash = '#me';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Only run showProfilePlayerFromUrl after session/profile checks are complete ---
|
||||
function runProfilePlayerIfSessionValid() {
|
||||
if (typeof checkSessionValidity === "function" && !checkSessionValidity()) return;
|
||||
showProfilePlayerFromUrl();
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
setTimeout(runProfilePlayerIfSessionValid, 200);
|
||||
});
|
||||
window.addEventListener('popstate', () => {
|
||||
setTimeout(runProfilePlayerIfSessionValid, 200);
|
||||
});
|
||||
window.showProfilePlayerFromUrl = showProfilePlayerFromUrl;
|
||||
|
||||
// Global audio state
|
||||
let globalAudio = null;
|
||||
let currentStreamUid = null;
|
||||
let audioPlaying = false;
|
||||
let lastPosition = 0;
|
||||
|
||||
// Expose main audio element for other scripts
|
||||
window.getMainAudio = () => globalAudio;
|
||||
window.stopMainAudio = () => {
|
||||
if (globalAudio) {
|
||||
globalAudio.pause();
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
||||
function getOrCreateAudioElement() {
|
||||
if (!globalAudio) {
|
||||
globalAudio = document.getElementById('me-audio');
|
||||
if (!globalAudio) {
|
||||
console.error('Audio element not found');
|
||||
return null;
|
||||
}
|
||||
// Set up audio element properties
|
||||
globalAudio.preload = 'metadata'; // Preload metadata for better performance
|
||||
globalAudio.crossOrigin = 'use-credentials'; // Use credentials for authenticated requests
|
||||
globalAudio.setAttribute('crossorigin', 'use-credentials'); // Explicitly set the attribute
|
||||
|
||||
// Set up event listeners
|
||||
globalAudio.addEventListener('play', () => {
|
||||
audioPlaying = true;
|
||||
updatePlayPauseButton();
|
||||
});
|
||||
globalAudio.addEventListener('pause', () => {
|
||||
audioPlaying = false;
|
||||
updatePlayPauseButton();
|
||||
});
|
||||
globalAudio.addEventListener('timeupdate', () => lastPosition = globalAudio.currentTime);
|
||||
|
||||
// Add error handling
|
||||
globalAudio.addEventListener('error', (e) => {
|
||||
console.error('Audio error:', e);
|
||||
showToast('❌ Audio playback error');
|
||||
});
|
||||
}
|
||||
return globalAudio;
|
||||
}
|
||||
|
||||
// Function to update play/pause button state
|
||||
function updatePlayPauseButton() {
|
||||
const audio = getOrCreateAudioElement();
|
||||
if (playPauseButton && audio) {
|
||||
playPauseButton.textContent = audio.paused ? '▶' : '⏸️';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize play/pause button
|
||||
const playPauseButton = document.getElementById('play-pause');
|
||||
if (playPauseButton) {
|
||||
// Set initial state
|
||||
updatePlayPauseButton();
|
||||
});
|
||||
|
||||
// Add click handler
|
||||
playPauseButton.addEventListener('click', () => {
|
||||
const audio = getOrCreateAudioElement();
|
||||
if (audio) {
|
||||
if (audio.paused) {
|
||||
// Stop any playing public streams first
|
||||
const publicPlayers = document.querySelectorAll('.stream-player audio');
|
||||
publicPlayers.forEach(player => {
|
||||
if (!player.paused) {
|
||||
player.pause();
|
||||
const button = player.closest('.stream-player').querySelector('.play-pause');
|
||||
if (button) {
|
||||
button.textContent = '▶';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
audio.play().catch(e => {
|
||||
console.error('Play failed:', e);
|
||||
audioPlaying = false;
|
||||
});
|
||||
} else {
|
||||
audio.pause();
|
||||
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;
|
||||
}
|
||||
updatePlayPauseButton();
|
||||
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'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Preload audio without playing it
|
||||
function preloadAudio(src) {
|
||||
return new Promise((resolve) => {
|
||||
const audio = new Audio();
|
||||
audio.preload = 'auto';
|
||||
audio.crossOrigin = 'anonymous';
|
||||
audio.src = src;
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Load and play a stream
|
||||
async function loadProfileStream(uid) {
|
||||
const audio = getOrCreateAudioElement();
|
||||
if (!audio) return null;
|
||||
|
||||
// Always reset current stream and update audio source
|
||||
currentStreamUid = uid;
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
|
||||
// Wait a moment to ensure the previous source is cleared
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Set new source with cache-busting timestamp
|
||||
audio.src = `/audio/${encodeURIComponent(uid)}/stream.opus?t=${Date.now()}`;
|
||||
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.error('Play failed:', 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
|
||||
// Show stream info if available
|
||||
const streamInfo = document.getElementById("stream-info");
|
||||
if (streamInfo) streamInfo.hidden = false;
|
||||
|
||||
// Update button state
|
||||
updatePlayPauseButton();
|
||||
|
||||
return audio;
|
||||
} 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;
|
||||
}
|
||||
|
||||
// Load and play a stream
|
||||
async function loadProfileStream(uid) {
|
||||
const audio = getOrCreateAudioElement();
|
||||
if (!audio) return null;
|
||||
|
||||
// Hide playlist controls
|
||||
const mePrevBtn = document.getElementById("me-prev");
|
||||
if (mePrevBtn) mePrevBtn.style.display = "none";
|
||||
const meNextBtn = document.getElementById("me-next");
|
||||
if (meNextBtn) meNextBtn.style.display = "none";
|
||||
|
||||
// Handle navigation to "Your Stream"
|
||||
const mePageLink = document.getElementById("show-me");
|
||||
if (mePageLink) {
|
||||
mePageLink.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const uid = localStorage.getItem("uid");
|
||||
if (!uid) return;
|
||||
|
||||
// Show loading state
|
||||
const streamInfo = document.getElementById("stream-info");
|
||||
if (streamInfo) {
|
||||
streamInfo.hidden = false;
|
||||
streamInfo.innerHTML = '<p>Loading stream...</p>';
|
||||
}
|
||||
|
||||
try {
|
||||
// Load the stream but don't autoplay
|
||||
await loadProfileStream(uid);
|
||||
|
||||
// Update URL without triggering a full page reload
|
||||
if (window.location.pathname !== '/') {
|
||||
window.history.pushState({}, '', '/');
|
||||
}
|
||||
|
||||
// Show the me-page section
|
||||
const mePage = document.getElementById('me-page');
|
||||
if (mePage) {
|
||||
document.querySelectorAll('main > section').forEach(s => s.hidden = s.id !== 'me-page');
|
||||
}
|
||||
|
||||
// Clear loading state
|
||||
const streamInfo = document.getElementById('stream-info');
|
||||
if (streamInfo) {
|
||||
streamInfo.innerHTML = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stream:', error);
|
||||
const streamInfo = document.getElementById('stream-info');
|
||||
if (streamInfo) {
|
||||
streamInfo.innerHTML = '<p>Error loading stream. Please try again.</p>';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Always reset current stream and update audio source
|
||||
currentStreamUid = uid;
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
|
||||
// Wait a moment to ensure the previous source is cleared
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Set new source with cache-busting timestamp
|
||||
audio.src = `/audio/${encodeURIComponent(uid)}/stream.opus?t=${Date.now()}`;
|
||||
|
||||
// Try to play immediately
|
||||
try {
|
||||
await audio.play();
|
||||
audioPlaying = true;
|
||||
} catch (e) {
|
||||
console.error('Play failed:', e);
|
||||
audioPlaying = false;
|
||||
}
|
||||
|
||||
// Show stream info
|
||||
const streamInfo = document.getElementById("stream-info");
|
||||
if (streamInfo) streamInfo.hidden = false;
|
||||
|
||||
|
||||
// Update button state
|
||||
updatePlayPauseButton();
|
||||
|
||||
updatePlayPauseButton(audio, document.querySelector('.play-pause-btn'));
|
||||
return audio;
|
||||
}
|
||||
|
||||
// Export the function for use in other modules
|
||||
window.loadProfileStream = loadProfileStream;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initialize play/pause button
|
||||
const playPauseButton = document.getElementById('play-pause');
|
||||
if (playPauseButton) {
|
||||
// Set initial state
|
||||
audioPlaying = false;
|
||||
updatePlayPauseButton();
|
||||
// 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;
|
||||
|
||||
// Add event listener
|
||||
playPauseButton.addEventListener('click', () => {
|
||||
const audio = getMainAudio();
|
||||
if (audio) {
|
||||
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() {
|
||||
const navLinks = document.querySelectorAll('nav a');
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
// Skip if href is empty or doesn't start with '#'
|
||||
if (!href || !href.startsWith('#')) {
|
||||
return; // Let the browser handle the link normally
|
||||
}
|
||||
|
||||
const sectionId = href.substring(1); // Remove the '#'
|
||||
|
||||
// Skip if sectionId is empty after removing '#'
|
||||
if (!sectionId) {
|
||||
console.warn('Empty section ID in navigation link:', link);
|
||||
return;
|
||||
}
|
||||
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide all sections first
|
||||
document.querySelectorAll('main > section').forEach(sec => {
|
||||
sec.hidden = sec.id !== sectionId;
|
||||
});
|
||||
|
||||
// Special handling for me-page
|
||||
if (sectionId === 'me-page') {
|
||||
const registerPage = document.getElementById('register-page');
|
||||
if (registerPage) registerPage.hidden = true;
|
||||
|
||||
// Show the upload box in me-page
|
||||
const uploadBox = document.querySelector('#me-page #user-upload-area');
|
||||
if (uploadBox) uploadBox.style.display = 'block';
|
||||
} else if (sectionId === 'register-page') {
|
||||
// Ensure me-page is hidden when register-page is shown
|
||||
const mePage = document.getElementById('me-page');
|
||||
if (mePage) mePage.hidden = true;
|
||||
}
|
||||
|
||||
section.scrollIntoView({ behavior: "smooth" });
|
||||
|
||||
// Close mobile menu if open
|
||||
const burger = document.getElementById('burger-toggle');
|
||||
if (burger && burger.checked) burger.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initProfilePlayer() {
|
||||
if (typeof checkSessionValidity === "function" && !checkSessionValidity()) return;
|
||||
showProfilePlayerFromUrl();
|
||||
}
|
||||
|
||||
// Initialize the application when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// 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) {
|
||||
audio.play();
|
||||
// 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);
|
||||
}
|
||||
updatePlayPauseButton();
|
||||
} catch (e) {
|
||||
console.error('Audio error:', e);
|
||||
updatePlayPauseButton(audio, playPauseBtn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add bot protection for registration form
|
||||
const registerForm = document.getElementById('register-form');
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', (e) => {
|
||||
const botTrap = e.target.elements.bot_trap;
|
||||
if (botTrap && botTrap.value) {
|
||||
|
||||
// Set up delete account button if it exists
|
||||
const deleteAccountBtn = document.getElementById('delete-account');
|
||||
if (deleteAccountBtn) {
|
||||
deleteAccountBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
showToast('❌ Bot detected! Please try again.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}, 200); // End of setTimeout
|
||||
});
|
||||
|
||||
// Expose functions for global access
|
||||
window.logToServer = logToServer;
|
||||
window.getMainAudio = () => globalAudio;
|
||||
window.stopMainAudio = () => {
|
||||
if (globalAudio) {
|
||||
globalAudio.pause();
|
||||
audioPlaying = false;
|
||||
updatePlayPauseButton();
|
||||
}
|
||||
|
||||
// Initialize navigation
|
||||
document.querySelectorAll('#links a[data-target]').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.getAttribute('data-target');
|
||||
// Only hide other sections when not opening #me-page
|
||||
if (target !== 'me-page') fadeAllSections();
|
||||
const section = document.getElementById(target);
|
||||
if (section) {
|
||||
section.hidden = false;
|
||||
section.classList.add("slide-in");
|
||||
section.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
const burger = document.getElementById('burger-toggle');
|
||||
if (burger && burger.checked) burger.checked = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize profile player if valid session
|
||||
setTimeout(runProfilePlayerIfSessionValid, 200);
|
||||
window.addEventListener('popstate', () => {
|
||||
setTimeout(runProfilePlayerIfSessionValid, 200);
|
||||
});
|
||||
});
|
||||
// Initialize navigation
|
||||
document.querySelectorAll('#links a[data-target]').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.getAttribute('data-target');
|
||||
// Only hide other sections when not opening #me-page
|
||||
if (target !== 'me-page') fadeAllSections();
|
||||
const section = document.getElementById(target);
|
||||
if (section) {
|
||||
section.hidden = false;
|
||||
section.classList.add("slide-in");
|
||||
section.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
const burger = document.getElementById('burger-toggle');
|
||||
if (burger && burger.checked) burger.checked = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize profile player if valid session
|
||||
setTimeout(runProfilePlayerIfSessionValid, 200);
|
||||
window.addEventListener('popstate', () => {
|
||||
setTimeout(runProfilePlayerIfSessionValid, 200);
|
||||
});
|
||||
});
|
||||
};
|
||||
window.loadProfileStream = loadProfileStream;
|
||||
|
Reference in New Issue
Block a user