Fix double audio playback and add UID handling for personal stream
- 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
This commit is contained in:
592
static/app.js
592
static/app.js
@ -2,6 +2,7 @@
|
||||
|
||||
import { playBeep } from "./sound.js";
|
||||
import { showToast } from "./toast.js";
|
||||
import { injectNavigation } from "./inject-nav.js";
|
||||
|
||||
// Global audio state
|
||||
let globalAudio = null;
|
||||
@ -30,29 +31,42 @@ 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 immediately without reload
|
||||
const guestDashboard = document.getElementById('guest-dashboard');
|
||||
const userDashboard = document.getElementById('user-dashboard');
|
||||
const registerPage = document.getElementById('register-page');
|
||||
// Update UI state
|
||||
document.body.classList.add('authenticated');
|
||||
document.body.classList.remove('guest');
|
||||
|
||||
if (guestDashboard) guestDashboard.style.display = 'none';
|
||||
if (userDashboard) userDashboard.style.display = 'block';
|
||||
if (registerPage) registerPage.style.display = 'none';
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,55 +312,116 @@ function showProfilePlayerFromUrl() {
|
||||
}
|
||||
|
||||
function initNavigation() {
|
||||
const navLinks = document.querySelectorAll('nav a');
|
||||
// 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', (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;
|
||||
}
|
||||
});
|
||||
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() {
|
||||
@ -354,14 +429,344 @@ function initProfilePlayer() {
|
||||
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();
|
||||
@ -453,26 +858,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
// Set up delete account button if it exists
|
||||
const deleteAccountBtn = document.getElementById('delete-account');
|
||||
if (deleteAccountBtn) {
|
||||
deleteAccountBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
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 (!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 = '/';
|
||||
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');
|
||||
@ -481,12 +887,48 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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;
|
||||
|
69
static/css/colors.css
Normal file
69
static/css/colors.css
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Color System Documentation
|
||||
*
|
||||
* This file documents the color variables used throughout the application.
|
||||
* All colors should be defined as CSS variables in :root, and these variables
|
||||
* should be used consistently across all CSS and JavaScript files.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--primary-color: #4a6fa5; /* Main brand color */
|
||||
--primary-hover: #3a5a8c; /* Darker shade for hover states */
|
||||
|
||||
/* Text Colors */
|
||||
--text-color: #f0f0f0; /* Main text color */
|
||||
--text-muted: #888; /* Secondary text, less important info */
|
||||
--text-light: #999; /* Lighter text for disabled states */
|
||||
--text-lighter: #bbb; /* Very light text, e.g., placeholders */
|
||||
|
||||
/* Background Colors */
|
||||
--background: #1a1a1a; /* Main background color */
|
||||
--surface: #2a2a2a; /* Surface color for cards, panels, etc. */
|
||||
--code-bg: #222; /* Background for code blocks */
|
||||
|
||||
/* Border Colors */
|
||||
--border: #444; /* Default border color */
|
||||
--border-light: #555; /* Lighter border */
|
||||
--border-lighter: #666; /* Even lighter border */
|
||||
|
||||
/* Status Colors */
|
||||
--success: #2e8b57; /* Success messages, confirmations */
|
||||
--warning: #ff6600; /* Warnings, important notices */
|
||||
--error: #ff4444; /* Error messages, destructive actions */
|
||||
--error-hover: #ff6666; /* Hover state for error buttons */
|
||||
--info: #1e90ff; /* Informational messages, links */
|
||||
--link-hover: #74c0fc; /* Hover state for links */
|
||||
|
||||
/* Transitions */
|
||||
--transition: all 0.2s ease; /* Default transition */
|
||||
}
|
||||
|
||||
/*
|
||||
* Usage Examples:
|
||||
*
|
||||
* .button {
|
||||
* background-color: var(--primary-color);
|
||||
* color: var(--text-color);
|
||||
* border: 1px solid var(--border);
|
||||
* transition: var(--transition);
|
||||
* }
|
||||
*
|
||||
* .button:hover {
|
||||
* background-color: var(--primary-hover);
|
||||
* }
|
||||
*
|
||||
* .error-message {
|
||||
* color: var(--error);
|
||||
* background-color: color-mix(in srgb, var(--error) 10%, transparent);
|
||||
* border-left: 3px solid var(--error);
|
||||
* }
|
||||
*/
|
||||
|
||||
/*
|
||||
* Accessibility Notes:
|
||||
* - Ensure text has sufficient contrast with its background
|
||||
* - Use semantic color names that describe the purpose, not the color
|
||||
* - Test with color blindness simulators for accessibility
|
||||
* - Maintain consistent color usage throughout the application
|
||||
*/
|
268
static/css/components/file-upload.css
Normal file
268
static/css/components/file-upload.css
Normal file
@ -0,0 +1,268 @@
|
||||
/* File upload and list styles */
|
||||
#user-upload-area {
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
#user-upload-area:hover,
|
||||
#user-upload-area.highlight {
|
||||
border-color: var(--primary);
|
||||
background-color: rgba(var(--primary-rgb), 0.05);
|
||||
}
|
||||
|
||||
#user-upload-area p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#file-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
#file-list {
|
||||
margin: 1.5rem 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#file-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
background-color: var(--surface);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#file-list li:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#file-list li.no-files,
|
||||
#file-list li.loading-message,
|
||||
#file-list li.error-message {
|
||||
display: block;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 2rem 1.5rem;
|
||||
background-color: transparent;
|
||||
border: 2px dashed var(--border);
|
||||
margin: 1rem 0;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#file-list li.loading-message {
|
||||
color: var(--primary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#file-list li.error-message {
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
#file-list li.error-message .login-link {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
#file-list li.error-message .login-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#file-list li.no-files:hover {
|
||||
background-color: rgba(var(--primary-rgb), 0.05);
|
||||
border-color: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0; /* Allows text truncation */
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1.2em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.file-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85em;
|
||||
margin-left: 0.5rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.download-button,
|
||||
.delete-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.download-button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background-color: transparent;
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background-color: rgba(var(--error-rgb), 0.1);
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show text on larger screens */
|
||||
@media (min-width: 640px) {
|
||||
.button-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.download-button,
|
||||
.delete-button {
|
||||
padding: 0.4rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 480px) {
|
||||
#file-list li {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#file-list li a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#file-list li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9em;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.delete-file {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--error);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.delete-file:hover {
|
||||
background-color: rgba(var(--error-rgb), 0.1);
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
#file-list.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
#user-upload-area {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
#file-list li {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/* Footer styles */
|
||||
footer {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
color: var(--text-color);
|
||||
padding: 2rem 0;
|
||||
margin-top: 3rem;
|
||||
width: 100%;
|
||||
@ -26,30 +26,30 @@ footer {
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: #ecf0f1;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-links a:hover,
|
||||
.footer-links a:focus {
|
||||
color: #3498db;
|
||||
color: var(--info);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #7f8c8d;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.footer-hint {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #bdc3c7;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.footer-hint a {
|
||||
color: #3498db;
|
||||
color: var(--info);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ header {
|
||||
.nav-link:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Active navigation item */
|
||||
|
54
static/css/section.css
Normal file
54
static/css/section.css
Normal file
@ -0,0 +1,54 @@
|
||||
/* section.css - Centralized visibility control with class-based states */
|
||||
|
||||
/* Base section visibility - all sections hidden by default */
|
||||
main > section {
|
||||
display: none;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Active section styling - only visibility properties */
|
||||
main > section.active {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Authentication-based visibility classes */
|
||||
.guest-only { display: block; }
|
||||
.auth-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show auth-only elements when authenticated */
|
||||
body.authenticated .auth-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Ensure me-page and its direct children are visible when me-page is active */
|
||||
#me-page:not([hidden]) > .auth-only,
|
||||
#me-page:not([hidden]) > section,
|
||||
#me-page:not([hidden]) > article,
|
||||
#me-page:not([hidden]) > div,
|
||||
/* Ensure account deletion section is visible when privacy page is active and user is authenticated */
|
||||
#privacy-page:not([hidden]) .auth-only,
|
||||
#privacy-page:not([hidden]) #account-deletion {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
body.authenticated .guest-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.always-visible {
|
||||
display: block !important;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,24 @@
|
||||
/* Desktop-specific styles for screens 960px and wider */
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--content-max-width: 800px;
|
||||
--content-padding: 1.25rem;
|
||||
--section-spacing: 1.5rem;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: #111 !important;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(188, 183, 107, 0.1) 0, /* Olive color */
|
||||
rgba(188, 183, 107, 0.1) 0,
|
||||
rgba(188, 183, 107, 0.1) 1px,
|
||||
transparent 1px,
|
||||
transparent 20px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(188, 183, 107, 0.1) 0, /* Olive color */
|
||||
rgba(188, 183, 107, 0.1) 0,
|
||||
rgba(188, 183, 107, 0.1) 1px,
|
||||
transparent 1px,
|
||||
transparent 20px
|
||||
@ -26,72 +32,200 @@
|
||||
body {
|
||||
background: transparent !important;
|
||||
min-height: 100vh !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Main content container */
|
||||
main {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--content-padding);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Ensure h2 in legal pages matches other pages */
|
||||
#privacy-page > article > h2:first-child,
|
||||
#imprint-page > article > h2:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* Streams Page Specific Styles */
|
||||
#streams-page section {
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.stream-card {
|
||||
margin-bottom: 1rem;
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.stream-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stream-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stream-card .card-content {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Section styles */
|
||||
section {
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto var(--section-spacing);
|
||||
background: rgba(26, 26, 26, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
section:hover {
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
nav.dashboard-nav {
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Desktop navigation visibility */
|
||||
nav.dashboard-nav {
|
||||
display: block;
|
||||
}
|
||||
/* Section styles are now handled in style.css */
|
||||
|
||||
/* Show desktop navigation */
|
||||
section#links {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide mobile navigation elements */
|
||||
#burger-label,
|
||||
#burger-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Dashboard navigation */
|
||||
#guest-dashboard,
|
||||
#user-dashboard {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
nav.dashboard-nav a {
|
||||
padding: 5px;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.5em;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Reset mobile-specific styles for desktop */
|
||||
.dashboard-nav {
|
||||
padding: 0.5em;
|
||||
max-width: none;
|
||||
justify-content: center;
|
||||
|
||||
nav.dashboard-nav a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-nav a {
|
||||
min-width: auto;
|
||||
font-size: 1rem;
|
||||
|
||||
/* Form elements */
|
||||
input[type="email"],
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
background: #2a2a2a;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button,
|
||||
.button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #4a6fa5;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.button:hover {
|
||||
background: #5a8ad4;
|
||||
}
|
||||
|
||||
/* Global article styles */
|
||||
main > section > article,
|
||||
#stream-page > article {
|
||||
#stream-page > article,
|
||||
#stream-page #stream-list > li .stream-player {
|
||||
max-width: 600px;
|
||||
margin: 0 auto 2em auto;
|
||||
margin: 2em auto 2em auto;
|
||||
padding: 2em;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Add top margin to all stream players except the first one */
|
||||
#stream-page #stream-list > li:not(:first-child) .stream-player {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Stream player styles */
|
||||
#stream-page #stream-list > li {
|
||||
list-style: none;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
#stream-page #stream-list > li .stream-player {
|
||||
padding: 1.5em;
|
||||
background: #1e1e1e;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Hover states - only apply to direct article children of sections */
|
||||
main > section > article:hover {
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(45deg, rgba(255, 102, 0, 0.05), rgba(255, 102, 0, 0.02));
|
||||
border: 1px solid #ff6600;
|
||||
#stream-page #stream-list {
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Stream player specific overrides can be added here if needed in the future */
|
||||
|
||||
/* Hover states moved to style.css for consistency */
|
||||
|
||||
/* Stream list desktop styles */
|
||||
#stream-list {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* User upload area desktop styles */
|
||||
/* User upload area - matches article styling */
|
||||
#user-upload-area {
|
||||
max-width: 600px !important;
|
||||
width: 100% !important;
|
||||
margin: 1.5rem auto !important;
|
||||
box-sizing: border-box !important;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 2rem auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
@ -10,19 +10,19 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="dicta2stream is a minimalist voice streaming platform for looping your spoken audio anonymously." />
|
||||
<title>dicta2stream</title>
|
||||
<!-- Responsive burger menu display -->
|
||||
<!-- Section visibility and navigation styles -->
|
||||
<link rel="stylesheet" href="/static/css/section.css" media="all" />
|
||||
|
||||
<style>
|
||||
#burger-label, #burger-toggle { display: none; }
|
||||
@media (max-width: 959px) {
|
||||
#burger-label { display: block; }
|
||||
section#links { display: none; }
|
||||
#burger-toggle:checked + #burger-label + section#links { display: block; }
|
||||
}
|
||||
/* Hide mobile menu by default on larger screens */
|
||||
@media (min-width: 960px) {
|
||||
section#links { display: block; }
|
||||
#mobile-menu { display: none !important; }
|
||||
#burger-label { display: none !important; }
|
||||
}
|
||||
</style>
|
||||
<link rel="modulepreload" href="/static/sound.js" />
|
||||
<script src="/static/streams-ui.js" type="module"></script>
|
||||
<script src="/static/app.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@ -33,50 +33,54 @@
|
||||
<main>
|
||||
|
||||
<!-- Guest Dashboard -->
|
||||
<nav id="guest-dashboard" class="dashboard-nav">
|
||||
<nav id="guest-dashboard" class="dashboard-nav guest-only">
|
||||
<a href="#welcome-page" id="guest-welcome">Welcome</a>
|
||||
<a href="#stream-page" id="guest-streams">Streams</a>
|
||||
<a href="#register-page" id="guest-login">Account</a>
|
||||
<a href="#account" id="guest-login">Account</a>
|
||||
</nav>
|
||||
|
||||
<!-- User Dashboard -->
|
||||
<nav id="user-dashboard" class="dashboard-nav" style="display:none;">
|
||||
<nav id="user-dashboard" class="dashboard-nav auth-only">
|
||||
<a href="#welcome-page" id="user-welcome">Welcome</a>
|
||||
<a href="#stream-page" id="user-streams">Streams</a>
|
||||
<a href="#me-page" id="show-me">Your Stream</a>
|
||||
</nav>
|
||||
<section id="me-page">
|
||||
<div style="position: relative; margin: 0 0 1.5rem 0; text-align: center;">
|
||||
<h2 style="margin: 0; padding: 0; line-height: 1; display: inline-block; position: relative; text-align: center;">
|
||||
Your Stream
|
||||
</h2>
|
||||
<div style="position: absolute; right: 0; top: 50%; transform: translateY(-50%); display: flex; gap: 0.5rem;">
|
||||
<button id="delete-account-button" class="delete-account-btn" style="font-size: 1rem; padding: 0.4em 0.8em; white-space: nowrap; display: none; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;">🗑️ Delete Account</button>
|
||||
<button id="logout-button" class="logout-btn" style="font-size: 1rem; padding: 0.4em 0.8em; white-space: nowrap; display: none;">🚪 LogOut</button>
|
||||
</div>
|
||||
<section id="me-page" class="auth-only">
|
||||
<div>
|
||||
<h2>Your Stream</h2>
|
||||
</div>
|
||||
<article>
|
||||
<p>This is your personal stream. Only you can upload to it.</p>
|
||||
<audio id="me-audio"></audio>
|
||||
<div class="audio-controls">
|
||||
<button class="play-pause-btn" type="button" aria-label="Play">▶️</button>
|
||||
<button class="play-pause-btn" type="button" aria-label="Play" data-uid="">▶️</button>
|
||||
</div>
|
||||
</article>
|
||||
<section id="user-upload-area" class="dropzone">
|
||||
<p>🎙 Drag & drop your audio file here<br>or click to browse</p>
|
||||
<section id="user-upload-area" class="auth-only">
|
||||
<p>Drag & drop your audio file here<br>or click to browse</p>
|
||||
<input type="file" id="fileInputUser" accept="audio/*" hidden />
|
||||
</section>
|
||||
|
||||
<article id="log-out" class="auth-only article--bordered logout-section">
|
||||
<button id="logout-button" class="button">🚪 Log Out</button>
|
||||
</article>
|
||||
|
||||
<section id="quota-meter" class="auth-only">
|
||||
<p class="quota-meter">Quota: <progress id="quota-bar" value="0" max="100"></progress> <span id="quota-text">0 MB</span></p>
|
||||
<h4>Uploaded Files</h4>
|
||||
<ul id="file-list" class="file-list">
|
||||
<li>Loading files...</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div id="spinner" class="spinner"></div>
|
||||
|
||||
|
||||
|
||||
<!-- Burger menu and legacy links section removed for clarity -->
|
||||
|
||||
<section id="terms-page" hidden>
|
||||
<section id="terms-page" class="always-visible">
|
||||
<h2>Terms of Service</h2>
|
||||
<article>
|
||||
<article class="article--bordered">
|
||||
<p>By accessing or using dicta2stream.net (the "Service"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree, do not use the Service.</p>
|
||||
<ul>
|
||||
<li>You must be at least 18 years old to register.</li>
|
||||
@ -90,31 +94,53 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="privacy-page" hidden>
|
||||
<h2>Privacy Policy</h2>
|
||||
<article>
|
||||
<section id="privacy-page" class="always-visible">
|
||||
<div>
|
||||
<h2>Privacy Policy</h2>
|
||||
</div>
|
||||
<article class="article--bordered">
|
||||
<ul>
|
||||
<li><strong>Users</strong>: Session uses both cookies and localStorage to store UID and authentication state.</li>
|
||||
<li><strong>Guests</strong>: No cookies are set. No persistent identifiers are stored.</li>
|
||||
<li>We log IP + UID only for abuse protection and quota enforcement.</li>
|
||||
<li>Uploads are scanned via Whisper+Ollama but not stored as transcripts.</li>
|
||||
<li>Data is never sold. Contact us for account deletion.</li>
|
||||
<li>Data is never sold.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<!-- This section will be shown only to authenticated users -->
|
||||
<div class="auth-only">
|
||||
<section id="account-deletion" class="article--bordered">
|
||||
<h3>Account Deletion</h3>
|
||||
<p>You can delete your account and all associated data at any time. This action is irreversible and will permanently remove:</p>
|
||||
<ul>
|
||||
<li>Your account information</li>
|
||||
<li>All uploaded audio files</li>
|
||||
</ul>
|
||||
|
||||
<div class="centered-container">
|
||||
<button id="delete-account-from-privacy" class="button">
|
||||
🗑️ Delete My Account
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Guest login message removed as per user request -->
|
||||
</section>
|
||||
|
||||
<section id="imprint-page" hidden>
|
||||
<section id="imprint-page" class="always-visible">
|
||||
<h2>Imprint</h2>
|
||||
<article>
|
||||
<article class="article--bordered">
|
||||
<p><strong>Andreas Michael Fleckl</strong></p>
|
||||
<p>Johnstrassse 7/6<br>1140 Vienna<br>Austria / Europe</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="welcome-page">
|
||||
<section id="welcome-page" class="always-visible">
|
||||
<h2>Welcome</h2>
|
||||
<article>
|
||||
<p>dicta2stream is a minimalist voice streaming platform for your spoken audio anonymously under a nickname in a loop. <br><br>
|
||||
<article class="article--bordered">
|
||||
<p>dicta2stream is a minimalist voice streaming platform for your spoken audio anonymously under a nickname in a loop. <span class="text-muted">(Opus | Mono | 48 kHz | 60 kbps)</span><br><br>
|
||||
<strong>What you can do here:</strong></p>
|
||||
<ul>
|
||||
<li>🎧 Listen to public voice streams from others, instantly</li>
|
||||
@ -122,51 +148,41 @@
|
||||
<li>🕵️ No sign-up required for listening</li>
|
||||
<li>🔒 Optional registration for uploading and managing your own stream</li>
|
||||
</ul>
|
||||
|
||||
<div class="email-section">
|
||||
<a href="mailto:Andreas.Fleckl@dicta2stream.net" class="button">
|
||||
Andreas.Fleckl@dicta2stream.net
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<section id="stream-page" hidden>
|
||||
<section id="stream-page" class="always-visible">
|
||||
<h2>Public Streams</h2>
|
||||
<!-- The list below is dynamically populated by streams-ui.js; shows 'Loading...' while fetching -->
|
||||
<ul id="stream-list"><li>Loading...</li></ul>
|
||||
</section>
|
||||
|
||||
<section id="register-page" hidden>
|
||||
<section id="register-page" class="guest-only">
|
||||
<h2>Account</h2>
|
||||
<article>
|
||||
<article class="article--wide">
|
||||
<form id="register-form">
|
||||
<p><label>Email<br><input type="email" name="email" required /></label></p>
|
||||
<p><label>Username<br><input type="text" name="user" required /></label></p>
|
||||
<p style="display: none;">
|
||||
<p class="bot-trap">
|
||||
<label>Leave this empty:<br>
|
||||
<input type="text" name="bot_trap" autocomplete="off" />
|
||||
</label>
|
||||
</p>
|
||||
<p><button type="submit">Login / Create Account</button></p>
|
||||
</form>
|
||||
<p><small>You’ll receive a magic login link via email. No password required.</small></p>
|
||||
<p style="font-size: 0.85em; opacity: 0.65; margin-top: 1em;">Your session expires after 1 hour. Shareable links redirect to homepage.</p>
|
||||
<p class="form-note">You'll receive a magic login link via email. No password required.</p>
|
||||
<p class="form-note session-note">Your session expires after 1 hour. Shareable links redirect to homepage.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="quota-meter" hidden>
|
||||
<p class="quota-meter">Quota: <progress id="quota-bar" value="0" max="100"></progress> <span id="quota-text">0 MB used</span></p>
|
||||
<div id="uploaded-files" style="margin-top: 10px; font-size: 0.9em;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">Uploaded Files:</div>
|
||||
<div id="file-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #333; padding: 5px; border-radius: 4px; background: #1a1a1a;">
|
||||
<div style="padding: 5px 0; color: #888; font-style: italic;">Loading files...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Built for public voice streaming • Opus | Mono | 48 kHz | 60 kbps</p>
|
||||
<p class="footer-hint">Need more space? Contact<a href="mailto:Andreas.Fleckl@dicta2stream.net">Andreas.Fleckl@dicta2stream.net</a></p>
|
||||
|
||||
<p class="footer-links">
|
||||
<a href="#" id="footer-terms" data-target="terms-page">Terms</a> |
|
||||
<a href="#" id="footer-privacy" data-target="privacy-page">Privacy</a> |
|
||||
@ -200,5 +216,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/static/init-personal-stream.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
35
static/init-personal-stream.js
Normal file
35
static/init-personal-stream.js
Normal file
@ -0,0 +1,35 @@
|
||||
// Initialize the personal stream play button with the user's UID
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Function to update the play button with UID
|
||||
function updatePersonalStreamPlayButton() {
|
||||
const playButton = document.querySelector('#me-page .play-pause-btn');
|
||||
if (!playButton) return;
|
||||
|
||||
// Get UID from localStorage or cookie
|
||||
const uid = localStorage.getItem('uid') || getCookie('uid');
|
||||
|
||||
if (uid) {
|
||||
// Set the data-uid attribute if not already set
|
||||
if (!playButton.dataset.uid) {
|
||||
playButton.dataset.uid = uid;
|
||||
console.log('[personal-stream] Set UID for personal stream play button:', uid);
|
||||
}
|
||||
} else {
|
||||
console.warn('[personal-stream] No UID found for personal stream play button');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get cookie value by name
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updatePersonalStreamPlayButton();
|
||||
|
||||
// Also update when auth state changes (e.g., after login)
|
||||
document.addEventListener('authStateChanged', updatePersonalStreamPlayButton);
|
||||
});
|
@ -1,136 +1,55 @@
|
||||
// inject-nav.js - Handles dynamic injection and management of navigation elements
|
||||
import { showOnly } from './router.js';
|
||||
|
||||
// Menu state
|
||||
let isMenuOpen = false;
|
||||
|
||||
// Export the injectNavigation function
|
||||
export function injectNavigation(isAuthenticated = false) {
|
||||
console.log('injectNavigation called with isAuthenticated:', isAuthenticated);
|
||||
const navContainer = document.getElementById('main-navigation');
|
||||
if (!navContainer) {
|
||||
console.error('Navigation container not found. Looking for #main-navigation');
|
||||
console.log('Available elements with id:', document.querySelectorAll('[id]'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
navContainer.innerHTML = '';
|
||||
// Function to set up guest navigation links
|
||||
function setupGuestNav() {
|
||||
const guestDashboard = document.getElementById('guest-dashboard');
|
||||
if (!guestDashboard) return;
|
||||
|
||||
console.log('Creating navigation...');
|
||||
try {
|
||||
// Create the navigation wrapper
|
||||
const navWrapper = document.createElement('nav');
|
||||
navWrapper.className = 'nav-wrapper';
|
||||
|
||||
// Create the navigation content
|
||||
const nav = isAuthenticated ? createUserNav() : createGuestNav();
|
||||
console.log('Navigation HTML created:', nav.outerHTML);
|
||||
|
||||
// Append navigation to wrapper
|
||||
navWrapper.appendChild(nav);
|
||||
|
||||
// Append to container
|
||||
navContainer.appendChild(navWrapper);
|
||||
|
||||
console.log('Navigation appended to container');
|
||||
|
||||
// Initialize menu toggle after navigation is injected
|
||||
setupMenuToggle();
|
||||
|
||||
// Set up menu links
|
||||
setupMenuLinks();
|
||||
|
||||
// Add click handler for the logo to navigate home
|
||||
const logo = document.querySelector('.logo');
|
||||
if (logo) {
|
||||
logo.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showOnly('welcome');
|
||||
closeMenu();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating navigation:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up menu toggle for mobile
|
||||
setupMenuToggle();
|
||||
|
||||
// Set up menu links
|
||||
setupMenuLinks();
|
||||
|
||||
// Close menu when clicking on a nav link on mobile
|
||||
const navLinks = navContainer.querySelectorAll('.nav-link');
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (window.innerWidth < 768) { // Mobile breakpoint
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add click handler for the logo to navigate home
|
||||
const logo = document.querySelector('.logo');
|
||||
if (logo) {
|
||||
logo.addEventListener('click', (e) => {
|
||||
const links = guestDashboard.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showOnly('welcome');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create the guest navigation
|
||||
function createGuestNav() {
|
||||
const nav = document.createElement('div');
|
||||
nav.className = 'dashboard-nav';
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', 'Main navigation');
|
||||
|
||||
const navList = document.createElement('ul');
|
||||
navList.className = 'nav-list';
|
||||
|
||||
const links = [
|
||||
{ id: 'nav-login', target: 'login', text: 'Login / Register' },
|
||||
{ id: 'nav-streams', target: 'streams', text: 'Streams' },
|
||||
{ id: 'nav-welcome', target: 'welcome', text: 'Welcome' }
|
||||
];
|
||||
|
||||
// Create and append links
|
||||
links.forEach((link) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'nav-item';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.id = link.id;
|
||||
a.href = `#${link.target}`;
|
||||
a.className = 'nav-link';
|
||||
a.setAttribute('data-target', link.target);
|
||||
a.textContent = link.text;
|
||||
|
||||
// Add click handler for navigation
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget.getAttribute('data-target');
|
||||
const target = link.getAttribute('href')?.substring(1); // Remove '#'
|
||||
if (target) {
|
||||
window.location.hash = target;
|
||||
if (window.router && typeof window.router.showOnly === 'function') {
|
||||
window.router.showOnly(target);
|
||||
}
|
||||
// Close menu on mobile after clicking a link
|
||||
if (window.innerWidth < 768) {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
navList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to set up user navigation links
|
||||
function setupUserNav() {
|
||||
const userDashboard = document.getElementById('user-dashboard');
|
||||
if (!userDashboard) return;
|
||||
|
||||
nav.appendChild(navList);
|
||||
return nav;
|
||||
const links = userDashboard.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
// Handle logout specially
|
||||
if (link.getAttribute('href') === '#logout') {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (window.handleLogout) {
|
||||
window.handleLogout();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Handle regular navigation
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.getAttribute('href')?.substring(1); // Remove '#'
|
||||
if (target) {
|
||||
window.location.hash = target;
|
||||
if (window.router && typeof window.router.showOnly === 'function') {
|
||||
window.router.showOnly(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createUserNav() {
|
||||
@ -150,7 +69,7 @@ function createUserNav() {
|
||||
];
|
||||
|
||||
// Create and append links
|
||||
links.forEach((link, index) => {
|
||||
links.forEach((link) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'nav-item';
|
||||
|
||||
@ -158,35 +77,24 @@ function createUserNav() {
|
||||
a.id = link.id;
|
||||
a.href = '#';
|
||||
a.className = 'nav-link';
|
||||
|
||||
// Special handling for logout
|
||||
if (link.target === 'logout') {
|
||||
a.href = '#';
|
||||
a.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
closeMenu();
|
||||
// Use the handleLogout function from dashboard.js if available
|
||||
if (typeof handleLogout === 'function') {
|
||||
await handleLogout();
|
||||
} else {
|
||||
// Fallback in case handleLogout is not available
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('uid');
|
||||
localStorage.removeItem('uid_time');
|
||||
localStorage.removeItem('confirmed_uid');
|
||||
document.cookie = 'uid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
window.location.href = '/';
|
||||
}
|
||||
window.location.href = '#';
|
||||
// Force reload to reset the app state
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
a.setAttribute('data-target', link.target);
|
||||
}
|
||||
|
||||
a.setAttribute('data-target', link.target);
|
||||
a.textContent = link.text;
|
||||
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget.getAttribute('data-target');
|
||||
if (target === 'logout') {
|
||||
if (window.handleLogout) {
|
||||
window.handleLogout();
|
||||
}
|
||||
} else if (target) {
|
||||
window.location.hash = target;
|
||||
if (window.router && typeof window.router.showOnly === 'function') {
|
||||
window.router.showOnly(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
navList.appendChild(li);
|
||||
});
|
||||
@ -195,243 +103,82 @@ function createUserNav() {
|
||||
return nav;
|
||||
}
|
||||
|
||||
// Set up menu toggle functionality
|
||||
function setupMenuToggle() {
|
||||
const menuToggle = document.querySelector('.menu-toggle');
|
||||
const navWrapper = document.querySelector('.nav-wrapper');
|
||||
|
||||
if (!menuToggle || !navWrapper) return;
|
||||
|
||||
menuToggle.addEventListener('click', toggleMenu);
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (isMenuOpen && !navWrapper.contains(e.target) && !menuToggle.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && isMenuOpen) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when resizing to desktop
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle mobile menu
|
||||
function toggleMenu(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const navWrapper = document.querySelector('.nav-wrapper');
|
||||
const menuToggle = document.querySelector('.menu-toggle');
|
||||
|
||||
if (!navWrapper || !menuToggle) return;
|
||||
|
||||
isMenuOpen = !isMenuOpen;
|
||||
|
||||
if (isMenuOpen) {
|
||||
// Open menu
|
||||
navWrapper.classList.add('active');
|
||||
menuToggle.setAttribute('aria-expanded', 'true');
|
||||
menuToggle.innerHTML = '✕';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Focus the first link in the menu for better keyboard navigation
|
||||
const firstLink = navWrapper.querySelector('a');
|
||||
if (firstLink) firstLink.focus();
|
||||
|
||||
// Add click outside handler
|
||||
document._handleClickOutside = (e) => {
|
||||
if (!navWrapper.contains(e.target) && e.target !== menuToggle) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', document._handleClickOutside);
|
||||
|
||||
// Add escape key handler
|
||||
document._handleEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', document._handleEscape);
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// Close menu function
|
||||
function closeMenu() {
|
||||
const navWrapper = document.querySelector('.nav-wrapper');
|
||||
const menuToggle = document.querySelector('.menu-toggle');
|
||||
|
||||
if (!navWrapper || !menuToggle) return;
|
||||
|
||||
isMenuOpen = false;
|
||||
navWrapper.classList.remove('active');
|
||||
menuToggle.setAttribute('aria-expanded', 'false');
|
||||
menuToggle.innerHTML = '☰';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove event listeners
|
||||
if (document._handleClickOutside) {
|
||||
document.removeEventListener('click', document._handleClickOutside);
|
||||
delete document._handleClickOutside;
|
||||
}
|
||||
|
||||
if (document._handleEscape) {
|
||||
document.removeEventListener('keydown', document._handleEscape);
|
||||
delete document._handleEscape;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize menu toggle on page load
|
||||
function initializeMenuToggle() {
|
||||
console.log('Initializing menu toggle...');
|
||||
const menuToggle = document.getElementById('menu-toggle');
|
||||
|
||||
if (!menuToggle) {
|
||||
console.error('Main menu toggle button not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Menu toggle button found:', menuToggle);
|
||||
|
||||
// Remove any existing click listeners
|
||||
const newToggle = menuToggle.cloneNode(true);
|
||||
if (menuToggle.parentNode) {
|
||||
menuToggle.parentNode.replaceChild(newToggle, menuToggle);
|
||||
console.log('Replaced menu toggle button');
|
||||
} else {
|
||||
console.error('Menu toggle has no parent node!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add click handler to the new toggle
|
||||
newToggle.addEventListener('click', function(event) {
|
||||
console.log('Menu toggle clicked!', event);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleMenu(event);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Also handle the header menu toggle if it exists
|
||||
const headerMenuToggle = document.getElementById('header-menu-toggle');
|
||||
if (headerMenuToggle) {
|
||||
console.log('Header menu toggle found, syncing with main menu');
|
||||
headerMenuToggle.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
newToggle.click(); // Trigger the main menu toggle
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOM fully loaded and parsed');
|
||||
|
||||
// Initialize navigation based on authentication state
|
||||
// This will be set by the main app after checking auth status
|
||||
if (window.initializeNavigation) {
|
||||
window.initializeNavigation();
|
||||
}
|
||||
|
||||
// Initialize menu toggle
|
||||
initializeMenuToggle();
|
||||
|
||||
// Also try to initialize after a short delay in case the DOM changes
|
||||
setTimeout(initializeMenuToggle, 500);
|
||||
});
|
||||
|
||||
// Navigation injection function
|
||||
export function injectNavigation(isAuthenticated = false) {
|
||||
console.log('Injecting navigation, isAuthenticated:', isAuthenticated);
|
||||
const container = document.getElementById('main-navigation');
|
||||
const navWrapper = document.querySelector('.nav-wrapper');
|
||||
// Get the appropriate dashboard element based on auth state
|
||||
const guestDashboard = document.getElementById('guest-dashboard');
|
||||
const userDashboard = document.getElementById('user-dashboard');
|
||||
|
||||
if (!container || !navWrapper) {
|
||||
console.error('Navigation elements not found. Looking for #main-navigation and .nav-wrapper');
|
||||
return null;
|
||||
if (isAuthenticated) {
|
||||
// Show user dashboard, hide guest dashboard
|
||||
if (guestDashboard) guestDashboard.style.display = 'none';
|
||||
if (userDashboard) userDashboard.style.display = 'block';
|
||||
document.body.classList.add('authenticated');
|
||||
document.body.classList.remove('guest-mode');
|
||||
} else {
|
||||
// Show guest dashboard, hide user dashboard
|
||||
if (guestDashboard) guestDashboard.style.display = 'block';
|
||||
if (userDashboard) userDashboard.style.display = 'none';
|
||||
document.body.classList.add('guest-mode');
|
||||
document.body.classList.remove('authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
// Store scroll position
|
||||
const scrollPosition = window.scrollY;
|
||||
|
||||
// Clear existing navigation
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create the appropriate navigation based on authentication status
|
||||
const nav = isAuthenticated ? createUserNav() : createGuestNav();
|
||||
|
||||
// Append the navigation to the container
|
||||
container.appendChild(nav);
|
||||
|
||||
// Set up menu toggle functionality
|
||||
setupMenuToggle();
|
||||
|
||||
// Set up navigation links
|
||||
setupMenuLinks();
|
||||
|
||||
// Show the appropriate page based on URL
|
||||
if (window.location.hash === '#streams' || window.location.pathname === '/streams') {
|
||||
showOnly('stream-page');
|
||||
if (typeof window.maybeLoadStreamsOnShow === 'function') {
|
||||
window.maybeLoadStreamsOnShow();
|
||||
}
|
||||
} else if (!window.location.hash || window.location.hash === '#') {
|
||||
// Show welcome page by default if no hash
|
||||
showOnly('welcome-page');
|
||||
}
|
||||
|
||||
// Restore scroll position
|
||||
window.scrollTo(0, scrollPosition);
|
||||
|
||||
return nav;
|
||||
} catch (error) {
|
||||
console.error('Error injecting navigation:', error);
|
||||
return null;
|
||||
}
|
||||
// Set up menu links and active state
|
||||
setupMenuLinks();
|
||||
updateActiveNav();
|
||||
|
||||
return isAuthenticated ? userDashboard : guestDashboard;
|
||||
}
|
||||
|
||||
// Set up menu links with click handlers
|
||||
function setupMenuLinks() {
|
||||
// Handle navigation link clicks
|
||||
document.addEventListener('click', function(e) {
|
||||
// Check if click is on a nav link or its children
|
||||
let link = e.target.closest('.nav-link');
|
||||
if (!link) return;
|
||||
|
||||
const target = link.getAttribute('data-target');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
console.log('Navigation link clicked:', target);
|
||||
showOnly(target);
|
||||
closeMenu();
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.nav-link').forEach(l => {
|
||||
l.classList.remove('active');
|
||||
});
|
||||
// Set up guest and user navigation links
|
||||
setupGuestNav();
|
||||
setupUserNav();
|
||||
|
||||
// Handle hash changes for SPA navigation
|
||||
window.addEventListener('hashchange', updateActiveNav);
|
||||
}
|
||||
|
||||
// Update active navigation link
|
||||
function updateActiveNav() {
|
||||
const currentHash = window.location.hash.substring(1) || 'welcome';
|
||||
|
||||
// Remove active class from all links in both dashboards
|
||||
document.querySelectorAll('#guest-dashboard a, #user-dashboard a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
// Check if this link's href matches the current hash
|
||||
const linkTarget = link.getAttribute('href')?.substring(1); // Remove '#'
|
||||
if (linkTarget === currentHash) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check authentication state and initialize navigation
|
||||
const isAuthenticated = document.cookie.includes('sessionid=') ||
|
||||
localStorage.getItem('isAuthenticated') === 'true';
|
||||
|
||||
// Initialize navigation based on authentication state
|
||||
injectNavigation(isAuthenticated);
|
||||
|
||||
// Set up menu links and active navigation
|
||||
setupMenuLinks();
|
||||
updateActiveNav();
|
||||
|
||||
// Update body classes based on authentication state
|
||||
if (isAuthenticated) {
|
||||
document.body.classList.add('authenticated');
|
||||
document.body.classList.remove('guest-mode');
|
||||
} else {
|
||||
document.body.classList.add('guest-mode');
|
||||
document.body.classList.remove('authenticated');
|
||||
}
|
||||
|
||||
console.log('[NAV] Navigation initialized', { isAuthenticated });
|
||||
});
|
||||
|
||||
// Make the function available globally for debugging
|
||||
window.injectNavigation = injectNavigation;
|
||||
|
@ -27,10 +27,17 @@ export async function initMagicLogin() {
|
||||
const url = new URL(res.url);
|
||||
const confirmedUid = url.searchParams.get('confirmed_uid');
|
||||
if (confirmedUid) {
|
||||
document.cookie = "uid=" + encodeURIComponent(confirmedUid) + "; path=/";
|
||||
// Set localStorage for SPA session logic instantly
|
||||
// Generate a simple auth token (in a real app, this would come from the server)
|
||||
const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// Set cookies and localStorage for SPA session logic
|
||||
document.cookie = `uid=${encodeURIComponent(confirmedUid)}; path=/`;
|
||||
document.cookie = `authToken=${authToken}; path=/`;
|
||||
|
||||
// Store in localStorage for client-side access
|
||||
localStorage.setItem('uid', confirmedUid);
|
||||
localStorage.setItem('confirmed_uid', confirmedUid);
|
||||
localStorage.setItem('authToken', authToken);
|
||||
localStorage.setItem('uid_time', Date.now().toString());
|
||||
}
|
||||
window.location.href = res.url;
|
||||
@ -42,10 +49,17 @@ export async function initMagicLogin() {
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await res.json();
|
||||
if (data && data.confirmed_uid) {
|
||||
document.cookie = "uid=" + encodeURIComponent(data.confirmed_uid) + "; path=/";
|
||||
// Set localStorage for SPA session logic
|
||||
// Generate a simple auth token (in a real app, this would come from the server)
|
||||
const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// Set cookies and localStorage for SPA session logic
|
||||
document.cookie = `uid=${encodeURIComponent(data.confirmed_uid)}; path=/`;
|
||||
document.cookie = `authToken=${authToken}; path=/`;
|
||||
|
||||
// Store in localStorage for client-side access
|
||||
localStorage.setItem('uid', data.confirmed_uid);
|
||||
localStorage.setItem('confirmed_uid', data.confirmed_uid);
|
||||
localStorage.setItem('authToken', authToken);
|
||||
localStorage.setItem('uid_time', Date.now().toString());
|
||||
import('./toast.js').then(({ showToast }) => {
|
||||
showToast('✅ Login successful!');
|
||||
|
@ -36,6 +36,145 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Mobile navigation */
|
||||
#user-dashboard.dashboard-nav {
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dashboard-nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0.5rem 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-nav a {
|
||||
padding: 0.5rem 0.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dashboard-nav a:hover,
|
||||
.dashboard-nav a:focus {
|
||||
background-color: var(--hover-bg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Account Deletion Section */
|
||||
#privacy-page.active #account-deletion,
|
||||
#privacy-page:not(.active) #account-deletion {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
position: relative !important;
|
||||
clip: auto !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.account-deletion-section {
|
||||
margin: 2rem 0;
|
||||
padding: 1.75rem;
|
||||
background: rgba(26, 26, 26, 0.8);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.account-deletion-section h3 {
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.account-deletion-section h3 {
|
||||
color: #fff;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.account-deletion-section ul {
|
||||
margin: 1.5rem 0 2rem 1.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.account-deletion-section li {
|
||||
margin-bottom: 0.75rem;
|
||||
color: #f0f0f0;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.account-deletion-section li:before {
|
||||
content: '•';
|
||||
color: #ff5e57;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -0.25rem;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
background: linear-gradient(135deg, #ff3b30, #ff5e57);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 59, 48, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.danger-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(255, 59, 48, 0.4);
|
||||
}
|
||||
|
||||
.danger-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: #4dabf7;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.text-link:hover {
|
||||
color: #74c0fc;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Hide desktop navigation in mobile */
|
||||
nav.dashboard-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
@ -98,15 +237,22 @@
|
||||
}
|
||||
|
||||
#quota-meter {
|
||||
margin: 1rem 0;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 1rem auto;
|
||||
padding: 0 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quota-meter {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Stream item styles moved to .stream-player */
|
||||
.stream-item {
|
||||
padding: 0.75rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@ -183,8 +329,8 @@
|
||||
nav.dashboard-nav a {
|
||||
all: unset;
|
||||
display: inline-block;
|
||||
background-color: #1e1e1e;
|
||||
color: #fff;
|
||||
background-color: var(--surface);
|
||||
color: var(--text-color);
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
@ -197,7 +343,7 @@
|
||||
}
|
||||
|
||||
.dashboard-nav a:active {
|
||||
background-color: #333;
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
/* Stream page specific styles */
|
||||
@ -215,11 +361,19 @@
|
||||
}
|
||||
|
||||
#stream-list {
|
||||
padding: 0.5rem;
|
||||
padding: 0 1rem;
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#stream-list li {
|
||||
margin-bottom: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.stream-player {
|
||||
@ -234,18 +388,22 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#stream-list > li {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
/* Stream list items are now handled by the rules above */
|
||||
|
||||
/* User upload area */
|
||||
/* User upload area - matches article styling */
|
||||
#user-upload-area {
|
||||
margin: 1rem 0;
|
||||
padding: 1.5rem;
|
||||
margin: 2rem auto;
|
||||
padding: 1.6875rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-color, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 2px dashed #666;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#user-upload-area p {
|
||||
@ -268,7 +426,7 @@
|
||||
|
||||
.stream-info {
|
||||
font-size: 0.9rem;
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@ -277,14 +435,31 @@
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #444;
|
||||
background-color: #2a2a2a;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Firefox mobile specific fixes */
|
||||
@-moz-document url-prefix() {
|
||||
input[type="email"] {
|
||||
min-height: 2.5rem;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust audio element for mobile */
|
||||
|
310
static/nav.js
310
static/nav.js
@ -8,62 +8,267 @@ function getCookie(name) {
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Check authentication status
|
||||
const isLoggedIn = !!getCookie('uid');
|
||||
|
||||
// Update body class for CSS-based visibility
|
||||
document.body.classList.toggle('logged-in', isLoggedIn);
|
||||
|
||||
// Get all main content sections
|
||||
const mainSections = Array.from(document.querySelectorAll('main > section'));
|
||||
|
||||
// Show/hide sections with smooth transitions
|
||||
const showSection = (sectionId) => {
|
||||
// Update body class to indicate current page
|
||||
document.body.className = '';
|
||||
if (sectionId) {
|
||||
document.body.classList.add(`page-${sectionId}`);
|
||||
} else {
|
||||
document.body.classList.add('page-welcome');
|
||||
}
|
||||
|
||||
// Update active state of navigation links
|
||||
document.querySelectorAll('.dashboard-nav a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if ((!sectionId && link.getAttribute('href') === '#welcome-page') ||
|
||||
(sectionId && link.getAttribute('href') === `#${sectionId}`)) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
mainSections.forEach(section => {
|
||||
// Skip navigation sections
|
||||
if (section.id === 'guest-dashboard' || section.id === 'user-dashboard') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTarget = section.id === sectionId;
|
||||
const isLegalPage = ['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId);
|
||||
const isWelcomePage = !sectionId || sectionId === 'welcome-page';
|
||||
|
||||
if (isTarget || (isLegalPage && section.id === sectionId)) {
|
||||
// Show the target section or legal page
|
||||
section.classList.add('active');
|
||||
section.hidden = false;
|
||||
|
||||
// Focus the section for accessibility with a small delay
|
||||
// Only focus if the section is focusable and in the viewport
|
||||
const focusSection = () => {
|
||||
try {
|
||||
if (section && typeof section.focus === 'function' &&
|
||||
section.offsetParent !== null && // Check if element is visible
|
||||
section.getBoundingClientRect().top < window.innerHeight &&
|
||||
section.getBoundingClientRect().bottom > 0) {
|
||||
section.focus({ preventScroll: true });
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail if focusing isn't possible
|
||||
if (window.DEBUG_NAV || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
||||
console.debug('Could not focus section:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use requestAnimationFrame for better performance
|
||||
requestAnimationFrame(() => {
|
||||
// Only set the timeout in debug mode or local development
|
||||
if (window.DEBUG_NAV || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
||||
setTimeout(focusSection, 50);
|
||||
} else {
|
||||
focusSection();
|
||||
}
|
||||
});
|
||||
} else if (isWelcomePage && section.id === 'welcome-page') {
|
||||
// Special handling for welcome page
|
||||
section.classList.add('active');
|
||||
section.hidden = false;
|
||||
} else {
|
||||
// Hide other sections
|
||||
section.classList.remove('active');
|
||||
section.hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL hash without page scroll
|
||||
if (sectionId && !['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId)) {
|
||||
if (sectionId === 'welcome-page') {
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
} else {
|
||||
history.replaceState(null, '', `#${sectionId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial page load
|
||||
const getValidSection = (sectionId) => {
|
||||
const protectedSections = ['me-page', 'register-page'];
|
||||
|
||||
// If not logged in and trying to access protected section
|
||||
if (!isLoggedIn && protectedSections.includes(sectionId)) {
|
||||
return 'welcome-page';
|
||||
}
|
||||
|
||||
// If section doesn't exist, default to welcome page
|
||||
if (!document.getElementById(sectionId)) {
|
||||
return 'welcome-page';
|
||||
}
|
||||
|
||||
return sectionId;
|
||||
};
|
||||
|
||||
// Process initial page load
|
||||
const initialPage = window.location.hash.substring(1) || 'welcome-page';
|
||||
const validSection = getValidSection(initialPage);
|
||||
|
||||
// Update URL if needed
|
||||
if (validSection !== initialPage) {
|
||||
window.location.hash = validSection;
|
||||
}
|
||||
|
||||
// Show the appropriate section
|
||||
showSection(validSection);
|
||||
|
||||
const Router = {
|
||||
sections: Array.from(document.querySelectorAll("main > section")),
|
||||
|
||||
showOnly(id) {
|
||||
// Define which sections are part of the 'Your Stream' section
|
||||
const yourStreamSections = ['me-page', 'register-page', 'quota-meter'];
|
||||
const isYourStreamSection = yourStreamSections.includes(id);
|
||||
// Validate the section ID
|
||||
const validId = getValidSection(id);
|
||||
|
||||
// Update URL if needed
|
||||
if (validId !== id) {
|
||||
window.location.hash = validId;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the requested section
|
||||
showSection(validId);
|
||||
|
||||
// Handle the quota meter visibility - only show with 'me-page'
|
||||
const quotaMeter = document.getElementById('quota-meter');
|
||||
if (quotaMeter) {
|
||||
quotaMeter.hidden = id !== 'me-page';
|
||||
quotaMeter.tabIndex = id === 'me-page' ? 0 : -1;
|
||||
quotaMeter.hidden = validId !== 'me-page';
|
||||
quotaMeter.tabIndex = validId === 'me-page' ? 0 : -1;
|
||||
}
|
||||
|
||||
// Check if user is logged in
|
||||
const isLoggedIn = !!getCookie('uid');
|
||||
|
||||
// Handle all sections
|
||||
this.sections.forEach(sec => {
|
||||
// Skip quota meter as it's already handled
|
||||
if (sec.id === 'quota-meter') return;
|
||||
|
||||
// Special handling for register page - only show to guests
|
||||
if (sec.id === 'register-page') {
|
||||
sec.hidden = isLoggedIn || id !== 'register-page';
|
||||
sec.tabIndex = (!isLoggedIn && id === 'register-page') ? 0 : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the section if it matches the target ID
|
||||
// OR if it's a 'Your Stream' section and we're in a 'Your Stream' context
|
||||
const isSectionInYourStream = yourStreamSections.includes(sec.id);
|
||||
const shouldShow = (sec.id === id) ||
|
||||
(isYourStreamSection && isSectionInYourStream);
|
||||
|
||||
sec.hidden = !shouldShow;
|
||||
sec.tabIndex = shouldShow ? 0 : -1;
|
||||
});
|
||||
// Show user-upload-area only when me-page is shown and user is logged in
|
||||
const userUpload = document.getElementById("user-upload-area");
|
||||
if (userUpload) {
|
||||
const uid = getCookie("uid");
|
||||
userUpload.style.display = (id === "me-page" && uid) ? '' : 'none';
|
||||
}
|
||||
localStorage.setItem("last_page", id);
|
||||
const target = document.getElementById(id);
|
||||
if (target) target.focus();
|
||||
// Update navigation active states
|
||||
this.updateActiveNav(validId);
|
||||
},
|
||||
init() {
|
||||
initNavLinks();
|
||||
initBackButtons();
|
||||
|
||||
initStreamLinks();
|
||||
|
||||
updateActiveNav(activeId) {
|
||||
// Update active states for navigation links
|
||||
document.querySelectorAll('.dashboard-nav a').forEach(link => {
|
||||
const target = link.getAttribute('href').substring(1);
|
||||
if (target === activeId) {
|
||||
link.setAttribute('aria-current', 'page');
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.removeAttribute('aria-current');
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
const showOnly = Router.showOnly.bind(Router);
|
||||
|
||||
// Initialize the router
|
||||
const router = Router;
|
||||
|
||||
// Handle section visibility based on authentication
|
||||
const updateSectionVisibility = (sectionId) => {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (!section) return;
|
||||
|
||||
// Skip navigation sections and quota meter
|
||||
if (['guest-dashboard', 'user-dashboard', 'quota-meter'].includes(sectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHash = window.location.hash.substring(1);
|
||||
const isLegalPage = ['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId);
|
||||
|
||||
// Special handling for legal pages - always show when in hash
|
||||
if (isLegalPage) {
|
||||
const isActive = sectionId === currentHash;
|
||||
section.hidden = !isActive;
|
||||
section.tabIndex = isActive ? 0 : -1;
|
||||
if (isActive) section.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for me-page - only show to authenticated users
|
||||
if (sectionId === 'me-page') {
|
||||
section.hidden = !isLoggedIn || currentHash !== 'me-page';
|
||||
section.tabIndex = (isLoggedIn && currentHash === 'me-page') ? 0 : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for register page - only show to guests
|
||||
if (sectionId === 'register-page') {
|
||||
section.hidden = isLoggedIn || currentHash !== 'register-page';
|
||||
section.tabIndex = (!isLoggedIn && currentHash === 'register-page') ? 0 : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// For other sections, show if they match the current section ID
|
||||
const isActive = sectionId === currentHash;
|
||||
section.hidden = !isActive;
|
||||
section.tabIndex = isActive ? 0 : -1;
|
||||
|
||||
if (isActive) {
|
||||
section.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the router
|
||||
router.init = function() {
|
||||
// Update visibility for all sections
|
||||
this.sections.forEach(section => {
|
||||
updateSectionVisibility(section.id);
|
||||
});
|
||||
|
||||
// Show user-upload-area only when me-page is shown and user is logged in
|
||||
const userUpload = document.getElementById("user-upload-area");
|
||||
if (userUpload) {
|
||||
const uid = getCookie("uid");
|
||||
userUpload.style.display = (window.location.hash === '#me-page' && uid) ? '' : 'none';
|
||||
}
|
||||
|
||||
// Store the current page
|
||||
localStorage.setItem("last_page", window.location.hash.substring(1));
|
||||
|
||||
// Initialize navigation
|
||||
initNavLinks();
|
||||
initBackButtons();
|
||||
initStreamLinks();
|
||||
|
||||
// Ensure proper focus management for accessibility
|
||||
const currentSection = document.querySelector('main > section:not([hidden])');
|
||||
if (currentSection) {
|
||||
currentSection.setAttribute('tabindex', '0');
|
||||
currentSection.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the router
|
||||
router.init();
|
||||
|
||||
// Handle footer links
|
||||
document.querySelectorAll('.footer-links a').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.target;
|
||||
if (target) {
|
||||
// Show the target section without updating URL hash
|
||||
showSection(target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export the showOnly function for global access
|
||||
window.showOnly = router.showOnly.bind(router);
|
||||
|
||||
// Make router available globally for debugging
|
||||
window.appRouter = router;
|
||||
|
||||
// Highlight active profile link on browser back/forward navigation
|
||||
function highlightActiveProfileLink() {
|
||||
@ -80,6 +285,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
window.addEventListener('popstate', () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const profileUid = params.get('profile');
|
||||
const currentPage = window.location.hash.substring(1) || 'welcome-page';
|
||||
|
||||
// Prevent unauthorized access to me-page
|
||||
if ((currentPage === 'me-page' || profileUid) && !getCookie('uid')) {
|
||||
history.replaceState(null, '', '#welcome-page');
|
||||
showOnly('welcome-page');
|
||||
return;
|
||||
}
|
||||
|
||||
if (profileUid) {
|
||||
showOnly('me-page');
|
||||
if (typeof window.showProfilePlayerFromUrl === 'function') {
|
||||
@ -227,5 +441,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
// Initialize Router
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
// Re-check authentication when tab becomes visible again
|
||||
if (!document.hidden && window.location.hash === '#me-page' && !getCookie('uid')) {
|
||||
window.location.hash = 'welcome-page';
|
||||
showOnly('welcome-page');
|
||||
}
|
||||
});
|
||||
|
||||
Router.init();
|
||||
});
|
||||
|
165
static/router.js
165
static/router.js
@ -1,15 +1,168 @@
|
||||
// static/router.js — core routing for SPA navigation
|
||||
export const Router = {
|
||||
sections: Array.from(document.querySelectorAll("main > section")),
|
||||
sections: [],
|
||||
// Map URL hashes to section IDs
|
||||
sectionMap: {
|
||||
'welcome': 'welcome-page',
|
||||
'streams': 'stream-page',
|
||||
'account': 'register-page',
|
||||
'login': 'login-page',
|
||||
'me': 'me-page',
|
||||
'your-stream': 'me-page' // Map 'your-stream' to 'me-page'
|
||||
},
|
||||
|
||||
init() {
|
||||
this.sections = Array.from(document.querySelectorAll("main > section"));
|
||||
// Set up hash change handler
|
||||
window.addEventListener('hashchange', this.handleHashChange.bind(this));
|
||||
// Initial route
|
||||
this.handleHashChange();
|
||||
},
|
||||
|
||||
handleHashChange() {
|
||||
let hash = window.location.hash.substring(1) || 'welcome';
|
||||
|
||||
// First check if the hash matches any direct section ID
|
||||
const directSection = this.sections.find(sec => sec.id === hash);
|
||||
|
||||
if (directSection) {
|
||||
// If it's a direct section ID match, show it directly
|
||||
this.showOnly(hash);
|
||||
} else {
|
||||
// Otherwise, use the section map
|
||||
const sectionId = this.sectionMap[hash] || hash;
|
||||
this.showOnly(sectionId);
|
||||
}
|
||||
},
|
||||
|
||||
showOnly(id) {
|
||||
this.sections.forEach(sec => {
|
||||
sec.hidden = sec.id !== id;
|
||||
sec.tabIndex = -1;
|
||||
if (!id) return;
|
||||
|
||||
// Update URL hash without triggering hashchange
|
||||
if (window.location.hash !== `#${id}`) {
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
}
|
||||
|
||||
const isAuthenticated = document.body.classList.contains('authenticated');
|
||||
const isMePage = id === 'me-page' || id === 'your-stream';
|
||||
|
||||
// Helper function to update section visibility
|
||||
const updateSection = (sec) => {
|
||||
const isTarget = sec.id === id;
|
||||
const isGuestOnly = sec.classList.contains('guest-only');
|
||||
const isAuthOnly = sec.classList.contains('auth-only');
|
||||
const isAlwaysVisible = sec.classList.contains('always-visible');
|
||||
const isQuotaMeter = sec.id === 'quota-meter';
|
||||
const isUserUploadArea = sec.id === 'user-upload-area';
|
||||
const isLogOut = sec.id === 'log-out';
|
||||
|
||||
// Determine if section should be visible
|
||||
let shouldShow = isTarget;
|
||||
|
||||
// Always show sections with always-visible class
|
||||
if (isAlwaysVisible) {
|
||||
shouldShow = true;
|
||||
}
|
||||
|
||||
// Handle guest-only sections
|
||||
if (isGuestOnly && isAuthenticated) {
|
||||
shouldShow = false;
|
||||
}
|
||||
|
||||
// Handle auth-only sections
|
||||
if (isAuthOnly && !isAuthenticated) {
|
||||
shouldShow = false;
|
||||
}
|
||||
|
||||
// Special case for me-page and its children
|
||||
const isChildOfMePage = sec.closest('#me-page') !== null;
|
||||
const shouldBeActive = isTarget ||
|
||||
(isQuotaMeter && isMePage) ||
|
||||
(isUserUploadArea && isMePage) ||
|
||||
(isLogOut && isMePage) ||
|
||||
(isChildOfMePage && isMePage);
|
||||
|
||||
// Update visibility and tab index
|
||||
sec.hidden = !shouldShow;
|
||||
sec.tabIndex = shouldShow ? 0 : -1;
|
||||
|
||||
// Update active state and ARIA attributes
|
||||
if (shouldBeActive) {
|
||||
sec.setAttribute('aria-current', 'page');
|
||||
sec.classList.add('active');
|
||||
|
||||
// Ensure target section is visible
|
||||
if (sec.hidden) {
|
||||
sec.style.display = 'block';
|
||||
sec.hidden = false;
|
||||
}
|
||||
|
||||
// Show all children of the active section
|
||||
if (isTarget) {
|
||||
sec.focus();
|
||||
// Make sure all auth-only children are visible
|
||||
const authChildren = sec.querySelectorAll('.auth-only');
|
||||
authChildren.forEach(child => {
|
||||
if (isAuthenticated) {
|
||||
child.style.display = '';
|
||||
child.hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
sec.removeAttribute('aria-current');
|
||||
sec.classList.remove('active');
|
||||
|
||||
// Reset display property for sections when not active
|
||||
if (shouldShow && !isAlwaysVisible) {
|
||||
sec.style.display = ''; // Reset to default from CSS
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update all sections
|
||||
this.sections.forEach(updateSection);
|
||||
|
||||
// Update active nav links
|
||||
document.querySelectorAll('[data-target], [href^="#"]').forEach(link => {
|
||||
let target = link.getAttribute('data-target');
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
// If no data-target, try to get from href
|
||||
if (!target && href) {
|
||||
// Remove any query parameters and # from the href
|
||||
const hash = href.split('?')[0].substring(1);
|
||||
// Use mapped section ID or the hash as is
|
||||
target = this.sectionMap[hash] || hash;
|
||||
}
|
||||
|
||||
// Check if this link points to the current section or its mapped equivalent
|
||||
const linkId = this.sectionMap[target] || target;
|
||||
const currentId = this.sectionMap[id] || id;
|
||||
|
||||
if (linkId === currentId) {
|
||||
link.setAttribute('aria-current', 'page');
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.removeAttribute('aria-current');
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Close mobile menu if open
|
||||
const menuToggle = document.querySelector('.menu-toggle');
|
||||
if (menuToggle && menuToggle.getAttribute('aria-expanded') === 'true') {
|
||||
menuToggle.setAttribute('aria-expanded', 'false');
|
||||
document.body.classList.remove('menu-open');
|
||||
}
|
||||
|
||||
localStorage.setItem("last_page", id);
|
||||
const target = document.getElementById(id);
|
||||
if (target) target.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize router when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Router.init();
|
||||
});
|
||||
|
||||
export const showOnly = Router.showOnly.bind(Router);
|
||||
|
@ -65,17 +65,26 @@ function loadAndRenderStreams() {
|
||||
let streams = [];
|
||||
let connectionTimeout = null;
|
||||
|
||||
// Close previous connection and clear any pending timeouts
|
||||
// Clean up previous connection and timeouts
|
||||
if (window._streamsSSE) {
|
||||
console.log('[streams-ui] Aborting previous connection');
|
||||
console.group('[streams-ui] Cleaning up previous connection');
|
||||
console.log('Previous connection exists, aborting...');
|
||||
if (window._streamsSSE.abort) {
|
||||
window._streamsSSE.abort();
|
||||
console.log('Previous connection aborted');
|
||||
} else {
|
||||
console.log('No abort method on previous connection');
|
||||
}
|
||||
window._streamsSSE = null;
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
if (connectionTimeout) {
|
||||
console.log('[streams-ui] Clearing previous connection timeout');
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = null;
|
||||
} else {
|
||||
console.log('[streams-ui] No previous connection timeout to clear');
|
||||
}
|
||||
|
||||
console.log(`[streams-ui] Creating fetch-based SSE connection to ${sseUrl}`);
|
||||
@ -87,14 +96,41 @@ function loadAndRenderStreams() {
|
||||
// Store the controller for cleanup
|
||||
window._streamsSSE = controller;
|
||||
|
||||
// Set a connection timeout
|
||||
connectionTimeout = setTimeout(() => {
|
||||
// Set a connection timeout with debug info
|
||||
const connectionStartTime = Date.now();
|
||||
const connectionTimeoutId = setTimeout(() => {
|
||||
if (!gotAny) {
|
||||
console.log('[streams-ui] Connection timeout reached, forcing retry...');
|
||||
// Only log in development (localhost) or if explicitly enabled
|
||||
const isLocalDevelopment = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1';
|
||||
if (isLocalDevelopment || window.DEBUG_STREAMS) {
|
||||
const duration = Date.now() - connectionStartTime;
|
||||
console.group('[streams-ui] Connection timeout reached');
|
||||
console.log(`Duration: ${duration}ms`);
|
||||
console.log('Current time:', new Date().toISOString());
|
||||
console.log('Streams received:', streams.length);
|
||||
console.log('Active intervals:', window.activeIntervals ? window.activeIntervals.size : 'N/A');
|
||||
console.log('Active timeouts:', window.activeTimeouts ? window.activeTimeouts.size : 'N/A');
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Clean up and retry with backoff
|
||||
controller.abort();
|
||||
loadAndRenderStreams();
|
||||
|
||||
// Only retry if we haven't exceeded max retries
|
||||
const retryCount = window.streamRetryCount || 0;
|
||||
if (retryCount < 3) { // Max 3 retries
|
||||
window.streamRetryCount = retryCount + 1;
|
||||
const backoffTime = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
|
||||
setTimeout(loadAndRenderStreams, backoffTime);
|
||||
} else if (process.env.NODE_ENV === 'development' || window.DEBUG_STREAMS) {
|
||||
console.warn('Max retries reached for stream loading');
|
||||
}
|
||||
}
|
||||
}, 10000); // 10 second timeout
|
||||
}, 15000); // 15 second timeout (increased from 10s)
|
||||
|
||||
// Store the timeout ID for cleanup
|
||||
connectionTimeout = connectionTimeoutId;
|
||||
|
||||
console.log('[streams-ui] Making fetch request to:', sseUrl);
|
||||
|
||||
@ -102,7 +138,7 @@ function loadAndRenderStreams() {
|
||||
|
||||
console.log('[streams-ui] Creating fetch request with URL:', sseUrl);
|
||||
|
||||
// Make the fetch request
|
||||
// Make the fetch request with proper error handling
|
||||
fetch(sseUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -112,7 +148,6 @@ function loadAndRenderStreams() {
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
signal: signal,
|
||||
// Add mode and redirect options for better error handling
|
||||
mode: 'cors',
|
||||
redirect: 'follow'
|
||||
})
|
||||
@ -200,44 +235,49 @@ function loadAndRenderStreams() {
|
||||
return reader.read().then(processStream);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[streams-ui] Fetch request failed:', error);
|
||||
// Only handle the error if it's not an AbortError (from our own abort)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[streams-ui] Request was aborted as expected');
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[streams-ui] Stream loading failed:', error);
|
||||
|
||||
// Log additional error details
|
||||
if (error.name === 'TypeError') {
|
||||
console.error('[streams-ui] This is likely a network error or CORS issue');
|
||||
if (error.message.includes('fetch')) {
|
||||
console.error('[streams-ui] The fetch request was blocked or failed to reach the server');
|
||||
}
|
||||
if (error.message.includes('CORS')) {
|
||||
console.error('[streams-ui] CORS error detected. Check server CORS configuration');
|
||||
}
|
||||
}
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[streams-ui] Request was aborted');
|
||||
} else {
|
||||
console.error('[streams-ui] Error details:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
constructor: error.constructor.name,
|
||||
errorCode: error.code,
|
||||
errorNumber: error.errno,
|
||||
response: error.response
|
||||
});
|
||||
// Show a user-friendly error message
|
||||
const ul = document.getElementById('stream-list');
|
||||
if (ul) {
|
||||
let errorMessage = 'Error loading streams. ';
|
||||
|
||||
// Show a user-friendly error message
|
||||
const ul = document.getElementById('stream-list');
|
||||
if (ul) {
|
||||
ul.innerHTML = `
|
||||
<li class="error">
|
||||
<p>Error loading streams. Please try again later.</p>
|
||||
<p><small>Technical details: ${error.name}: ${error.message}</small></p>
|
||||
</li>
|
||||
`;
|
||||
if (error.message.includes('Failed to fetch')) {
|
||||
errorMessage += 'Unable to connect to the server. Please check your internet connection.';
|
||||
} else if (error.message.includes('CORS')) {
|
||||
errorMessage += 'A server configuration issue occurred. Please try again later.';
|
||||
} else {
|
||||
errorMessage += 'Please try again later.';
|
||||
}
|
||||
|
||||
handleSSEError(error);
|
||||
ul.innerHTML = `
|
||||
<li class="error">
|
||||
<p>${errorMessage}</p>
|
||||
<button id="retry-loading" class="retry-button">
|
||||
<span class="retry-icon">↻</span> Try Again
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// Add retry handler
|
||||
const retryButton = document.getElementById('retry-loading');
|
||||
if (retryButton) {
|
||||
retryButton.addEventListener('click', () => {
|
||||
ul.innerHTML = '<li>Loading streams...</li>';
|
||||
loadAndRenderStreams();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -279,7 +319,7 @@ function loadAndRenderStreams() {
|
||||
<div class="audio-controls">
|
||||
<button class="play-pause-btn" data-uid="${escapeHtml(uid)}" aria-label="Play">▶️</button>
|
||||
</div>
|
||||
<p class="stream-info" style='color:gray;font-size:90%'>[${sizeMb} MB, ${mtime}]</p>
|
||||
<p class="stream-info" style='color:var(--text-muted);font-size:90%'>[${sizeMb} MB, ${mtime}]</p>
|
||||
</article>
|
||||
`;
|
||||
ul.appendChild(li);
|
||||
@ -288,7 +328,7 @@ function loadAndRenderStreams() {
|
||||
console.error(`[streams-ui] Error rendering stream ${uid}:`, error);
|
||||
const errorLi = document.createElement('li');
|
||||
errorLi.textContent = `Error loading stream: ${uid}`;
|
||||
errorLi.style.color = 'red';
|
||||
errorLi.style.color = 'var(--error)';
|
||||
ul.appendChild(errorLi);
|
||||
}
|
||||
});
|
||||
@ -352,7 +392,7 @@ export function renderStreamList(streams) {
|
||||
const uid = stream.uid || '';
|
||||
const sizeKb = stream.size ? (stream.size / 1024).toFixed(1) : '?';
|
||||
const mtime = stream.mtime ? new Date(stream.mtime * 1000).toLocaleString() : '';
|
||||
return `<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${uid}</a> <span style='color:gray;font-size:90%'>[${sizeKb} KB, ${mtime}]</span></li>`;
|
||||
return `<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${uid}</a> <span style='color:var(--text-muted);font-size:90%'>[${sizeKb} KB, ${mtime}]</span></li>`;
|
||||
})
|
||||
.join('');
|
||||
} else {
|
||||
@ -704,50 +744,55 @@ function cleanupAudio() {
|
||||
}
|
||||
}
|
||||
|
||||
// Event delegation for play/pause buttons
|
||||
document.addEventListener('click', async (e) => {
|
||||
const playPauseBtn = e.target.closest('.play-pause-btn');
|
||||
if (!playPauseBtn) return;
|
||||
// Event delegation for play/pause buttons - only handle buttons within the stream list
|
||||
const streamList = document.getElementById('stream-list');
|
||||
if (streamList) {
|
||||
streamList.addEventListener('click', async (e) => {
|
||||
const playPauseBtn = e.target.closest('.play-pause-btn');
|
||||
// Skip if not a play button or if it's the personal stream's play button
|
||||
if (!playPauseBtn || playPauseBtn.closest('#me-page')) return;
|
||||
|
||||
// Prevent event from bubbling up to document-level handlers
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Prevent default to avoid any potential form submission or link following
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const uid = playPauseBtn.dataset.uid;
|
||||
if (!uid) {
|
||||
console.error('No UID found for play button');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[streams-ui] Play/pause clicked for UID: ${uid}, currentUid: ${currentUid}, isPlaying: ${isPlaying}`);
|
||||
|
||||
// If clicking the currently playing button, toggle pause/play
|
||||
if (currentUid === uid) {
|
||||
if (isPlaying) {
|
||||
console.log('[streams-ui] Pausing current audio');
|
||||
await audioElement.pause();
|
||||
isPlaying = false;
|
||||
updatePlayPauseButton(playPauseBtn, false);
|
||||
} else {
|
||||
console.log('[streams-ui] Resuming current audio');
|
||||
try {
|
||||
await audioElement.play();
|
||||
isPlaying = true;
|
||||
updatePlayPauseButton(playPauseBtn, true);
|
||||
} catch (error) {
|
||||
console.error('[streams-ui] Error resuming audio:', error);
|
||||
// If resume fails, try reloading the audio
|
||||
await loadAndPlayAudio(uid, playPauseBtn);
|
||||
}
|
||||
const uid = playPauseBtn.dataset.uid;
|
||||
if (!uid) {
|
||||
console.error('No UID found for play button');
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If a different stream is playing, stop it and start the new one
|
||||
console.log(`[streams-ui] Switching to new audio stream: ${uid}`);
|
||||
stopPlayback();
|
||||
await loadAndPlayAudio(uid, playPauseBtn);
|
||||
});
|
||||
|
||||
console.log(`[streams-ui] Play/pause clicked for UID: ${uid}, currentUid: ${currentUid}, isPlaying: ${isPlaying}`);
|
||||
|
||||
// If clicking the currently playing button, toggle pause/play
|
||||
if (currentUid === uid) {
|
||||
if (isPlaying) {
|
||||
console.log('[streams-ui] Pausing current audio');
|
||||
await audioElement.pause();
|
||||
isPlaying = false;
|
||||
updatePlayPauseButton(playPauseBtn, false);
|
||||
} else {
|
||||
console.log('[streams-ui] Resuming current audio');
|
||||
try {
|
||||
await audioElement.play();
|
||||
isPlaying = true;
|
||||
updatePlayPauseButton(playPauseBtn, true);
|
||||
} catch (error) {
|
||||
console.error('[streams-ui] Error resuming audio:', error);
|
||||
// If resume fails, try reloading the audio
|
||||
await loadAndPlayAudio(uid, playPauseBtn);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If a different stream is playing, stop it and start the new one
|
||||
console.log(`[streams-ui] Switching to new audio stream: ${uid}`);
|
||||
stopPlayback();
|
||||
await loadAndPlayAudio(uid, playPauseBtn);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle audio end event to update button state
|
||||
document.addEventListener('play', (e) => {
|
||||
|
1059
static/style.css
1059
static/style.css
File diff suppressed because it is too large
Load Diff
@ -5,25 +5,43 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audio Player Test</title>
|
||||
<style>
|
||||
:root {
|
||||
--success: #2e8b57;
|
||||
--error: #ff4444;
|
||||
--border: #444;
|
||||
--text-color: #f0f0f0;
|
||||
--surface: #2a2a2a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
background: #1a1a1a;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.test-case {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
.success { color: var(--success); }
|
||||
.error { color: var(--error); }
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
background: #4a6fa5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
background: #3a5a8c;
|
||||
}
|
||||
#log {
|
||||
margin-top: 20px;
|
||||
|
@ -5,25 +5,43 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audio Player Test</title>
|
||||
<style>
|
||||
:root {
|
||||
--success: #2e8b57;
|
||||
--error: #ff4444;
|
||||
--border: #444;
|
||||
--text-color: #f0f0f0;
|
||||
--surface: #2a2a2a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
background: #1a1a1a;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.test-case {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
.success { color: var(--success); }
|
||||
.error { color: var(--error); }
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
background: #4a6fa5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
background: #3a5a8c;
|
||||
}
|
||||
#log {
|
||||
margin-top: 20px;
|
||||
|
121
static/upload.js
121
static/upload.js
@ -112,8 +112,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
// Function to fetch and display uploaded files
|
||||
async function fetchAndDisplayFiles(uid) {
|
||||
console.log('[DEBUG] fetchAndDisplayFiles called with uid:', uid);
|
||||
async function fetchAndDisplayFiles(uidFromParam) {
|
||||
console.log('[UPLOAD] fetchAndDisplayFiles called with uid:', uidFromParam);
|
||||
|
||||
// Get the file list element
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) {
|
||||
const errorMsg = 'File list element not found in DOM';
|
||||
@ -121,12 +123,47 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return showErrorInUI(errorMsg);
|
||||
}
|
||||
|
||||
// Get UID from parameter, localStorage, or cookie
|
||||
const uid = uidFromParam || localStorage.getItem('uid') || getCookie('uid');
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
// Include auth token in headers if available, but don't fail if it's not
|
||||
// The server should handle both token-based and UID-based auth
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
} else {
|
||||
console.debug('[UPLOAD] No auth token available, using UID-only authentication');
|
||||
}
|
||||
|
||||
console.log('[UPLOAD] Auth state - UID:', uid, 'Token exists:', !!authToken);
|
||||
|
||||
if (!uid) {
|
||||
console.error('[UPLOAD] No UID found in any source');
|
||||
fileList.innerHTML = '<li class="error-message">User session expired. Please refresh the page.</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the authentication method being used
|
||||
if (!authToken) {
|
||||
console.debug('[UPLOAD] No auth token found, using UID-only authentication');
|
||||
} else {
|
||||
console.debug('[UPLOAD] Using token-based authentication');
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
fileList.innerHTML = '<div style="padding: 10px; color: #888; font-style: italic;">Loading files...</div>';
|
||||
fileList.innerHTML = '<li class="loading-message">Loading files...</li>';
|
||||
|
||||
try {
|
||||
console.log(`[DEBUG] Fetching files for user: ${uid}`);
|
||||
const response = await fetch(`/me/${uid}`);
|
||||
const response = await fetch(`/me/${uid}`, {
|
||||
headers: {
|
||||
'Authorization': authToken ? `Bearer ${authToken}` : '',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
console.log('[DEBUG] Response status:', response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
@ -152,21 +189,63 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const displayName = file.original_name || file.name;
|
||||
const isRenamed = file.original_name && file.original_name !== file.name;
|
||||
return `
|
||||
<div style="display: flex; justify-content: space-between; padding: 3px 0; border-bottom: 1px solid #2a2a2a;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${displayName}">
|
||||
${displayName}
|
||||
${isRenamed ? `<div style="font-size: 0.8em; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="Stored as: ${file.name}">${file.name}</div>` : ''}
|
||||
</div>
|
||||
<li class="file-item" data-filename="${file.name}">
|
||||
<div class="file-name" title="${displayName}">
|
||||
${displayName}
|
||||
${isRenamed ? `<div class="stored-as" title="Stored as: ${file.name}">${file.name} <button class="delete-file" data-filename="${file.name}" title="Delete file">🗑️</button></div>` :
|
||||
`<button class="delete-file" data-filename="${file.name}" title="Delete file">🗑️</button>`}
|
||||
</div>
|
||||
<span style="color: #888; white-space: nowrap; margin-left: 10px;">${sizeMB} MB</span>
|
||||
</div>
|
||||
<span class="file-size">${sizeMB} MB</span>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
fileList.innerHTML = '<div style="padding: 5px 0; color: #888; font-style: italic;">No files uploaded yet</div>';
|
||||
fileList.innerHTML = '<li class="empty-message">No files uploaded yet</li>';
|
||||
}
|
||||
|
||||
// Add event listeners to delete buttons
|
||||
document.querySelectorAll('.delete-file').forEach(button => {
|
||||
button.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const filename = button.dataset.filename;
|
||||
if (confirm(`Are you sure you want to delete ${filename}?`)) {
|
||||
try {
|
||||
// Get the auth token from the cookie
|
||||
const token = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('sessionid='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`/delete/${filename}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `Failed to delete file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Refresh the file list
|
||||
const uid = document.body.dataset.userUid;
|
||||
if (uid) {
|
||||
fetchAndDisplayFiles(uid);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
alert('Failed to delete file. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update quota display if available
|
||||
if (data.quota !== undefined) {
|
||||
const bar = document.getElementById('quota-bar');
|
||||
@ -176,7 +255,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
quotaSec.hidden = false;
|
||||
bar.value = data.quota;
|
||||
bar.max = 100;
|
||||
text.textContent = `${data.quota.toFixed(1)} MB used`;
|
||||
text.textContent = `${data.quota.toFixed(1)} MB`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -193,15 +272,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
margin: 5px 0;
|
||||
background: #2a0f0f;
|
||||
border-left: 3px solid #f55;
|
||||
color: #ff9999;
|
||||
color: var(--error-hover);
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
">
|
||||
<div style="font-weight: bold; color: #f55;">Error loading files</div>
|
||||
<div style="font-weight: bold; color: var(--error);">Error loading files</div>
|
||||
<div style="margin-top: 5px;">${message}</div>
|
||||
<div style="margin-top: 10px; font-size: 0.8em; color: #888;">
|
||||
<div style="margin-top: 10px; font-size: 0.8em; color: var(--text-muted);">
|
||||
Check browser console for details
|
||||
</div>
|
||||
</div>
|
||||
@ -217,6 +296,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get cookie value by name
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Export functions for use in other modules
|
||||
window.upload = upload;
|
||||
window.fetchAndDisplayFiles = fetchAndDisplayFiles;
|
||||
|
Reference in New Issue
Block a user