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:
oib
2025-07-18 16:51:39 +02:00
parent 17616ac5b8
commit 402e920bc6
24 changed files with 4074 additions and 1090 deletions

212
deletefile.py Normal file
View File

@ -0,0 +1,212 @@
# deletefile.py — FastAPI route for file deletion
import os
import shutil
from typing import Optional, Dict, Any
from pathlib import Path
from fastapi import APIRouter, HTTPException, Request, Depends, status, Header
from sqlalchemy import select, delete, and_
from sqlalchemy.orm import Session
from database import get_db
from models import UploadLog, UserQuota, User, DBSession
router = APIRouter()
# Use absolute path for security
DATA_ROOT = Path(os.path.abspath("./data"))
def get_current_user(
authorization: str = Header(None, description="Bearer token for authentication"),
db: Session = Depends(get_db)
) -> User:
"""
Get current user from authorization token with enhanced security.
Args:
authorization: The Authorization header containing the Bearer token
db: Database session dependency
Returns:
User: The authenticated user
Raises:
HTTPException: If authentication fails or user not found
"""
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
token = authorization.split(" ")[1]
try:
with Session(db) as session:
# Check if session is valid
session_stmt = select(DBSession).where(
and_(
DBSession.token == token,
DBSession.is_active == True,
DBSession.expires_at > datetime.utcnow()
)
)
db_session = session.exec(session_stmt).first()
if not db_session:
print(f"[DELETE_FILE] Invalid or expired session token")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired session"
)
# Get the user
user = session.get(User, db_session.user_id)
if not user:
print(f"[DELETE_FILE] User not found for session token")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
except Exception as e:
print(f"[DELETE_FILE] Error during user authentication: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error during authentication"
)
@router.delete("/delete/{filename}")
async def delete_file(
request: Request,
filename: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
) -> Dict[str, Any]:
"""
Delete a file for the authenticated user with enhanced security and error handling.
Args:
request: The HTTP request object
filename: The name of the file to delete
db: Database session
current_user: The authenticated user
Returns:
Dict: Status and message of the operation
Raises:
HTTPException: If file not found, permission denied, or other errors
"""
print(f"[DELETE_FILE] Processing delete request for file '{filename}' from user {current_user.username}")
try:
# Security: Validate filename to prevent directory traversal
if not filename or any(c in filename for c in ['..', '/', '\\']):
print(f"[DELETE_FILE] Security alert: Invalid filename '{filename}'")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid filename"
)
# Construct full path with security checks
user_dir = DATA_ROOT / current_user.username
file_path = (user_dir / filename).resolve()
# Security: Ensure the file is within the user's directory
if not file_path.is_relative_to(user_dir.resolve()):
print(f"[DELETE_FILE] Security alert: Attempted path traversal: {file_path}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Verify file exists and is a file
if not file_path.exists() or not file_path.is_file():
print(f"[DELETE_FILE] File not found: {file_path}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found"
)
# Get file size before deletion for quota update
file_size = file_path.stat().st_size
print(f"[DELETE_FILE] Deleting file: {file_path} (size: {file_size} bytes)")
# Start database transaction
with Session(db) as session:
try:
# Delete the file
try:
os.unlink(file_path)
print(f"[DELETE_FILE] Successfully deleted file: {file_path}")
except OSError as e:
print(f"[DELETE_FILE] Error deleting file: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete file"
)
# Clean up any associated raw files
raw_pattern = f"raw.*{filename}"
raw_files = list(file_path.parent.glob(raw_pattern))
for raw_file in raw_files:
try:
os.unlink(raw_file)
print(f"[DELETE_FILE] Deleted raw file: {raw_file}")
except OSError as e:
print(f"[DELETE_FILE] Warning: Could not delete raw file {raw_file}: {str(e)}")
# Delete the upload log entry
result = session.execute(
delete(UploadLog).where(
and_(
UploadLog.uid == current_user.username,
UploadLog.processed_filename == filename
)
)
)
if result.rowcount == 0:
print(f"[DELETE_FILE] Warning: No upload log entry found for {filename}")
else:
print(f"[DELETE_FILE] Deleted upload log entry for {filename}")
# Update user quota
quota = session.exec(
select(UserQuota)
.where(UserQuota.uid == current_user.username)
.with_for_update()
).first()
if quota:
new_quota = max(0, quota.storage_bytes - file_size)
print(f"[DELETE_FILE] Updating quota: {quota.storage_bytes} -> {new_quota}")
quota.storage_bytes = new_quota
session.add(quota)
session.commit()
print(f"[DELETE_FILE] Successfully updated database")
return {
"status": "success",
"message": "File deleted successfully",
"bytes_freed": file_size
}
except Exception as e:
session.rollback()
print(f"[DELETE_FILE] Database error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database error during file deletion"
)
except HTTPException as he:
print(f"[DELETE_FILE] HTTP Error {he.status_code}: {he.detail}")
raise
except Exception as e:
print(f"[DELETE_FILE] Unexpected error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected error occurred"
)

34
main.py
View File

@ -36,7 +36,19 @@ from fastapi.requests import Request as FastAPIRequest
from fastapi.exception_handlers import RequestValidationError
from fastapi.exceptions import HTTPException as FastAPIHTTPException
app = FastAPI(debug=debug_mode)
app = FastAPI(debug=debug_mode, docs_url=None, redoc_url=None, openapi_url=None)
# Override default HTML error handlers to return JSON
from fastapi.exceptions import RequestValidationError, HTTPException as FastAPIHTTPException
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail}
)
# --- CORS Middleware for SSE and API access ---
from fastapi.middleware.cors import CORSMiddleware
@ -292,10 +304,15 @@ def get_me(uid: str, request: Request, db: Session = Depends(get_db)):
user = get_user_by_uid(uid)
if not user:
print(f"[ERROR] User with UID {uid} not found")
raise HTTPException(status_code=403, detail="User not found")
raise HTTPException(status_code=404, detail="User not found")
# Only enforce IP check in production
if not debug_mode:
if user.ip != request.client.host:
print(f"[ERROR] IP mismatch for UID {uid}: {request.client.host} != {user.ip}")
print(f"[WARNING] IP mismatch for UID {uid}: {request.client.host} != {user.ip}")
# In production, we might want to be more strict
# But for now, we'll just log a warning in development
if not debug_mode:
raise HTTPException(status_code=403, detail="IP address mismatch")
# Get all upload logs for this user
@ -331,6 +348,13 @@ def get_me(uid: str, request: Request, db: Session = Depends(get_db)):
print(f"[DEBUG] Returning {len(files)} files and quota info")
return response_data
except Exception as e:
print(f"[ERROR] Error in /me/{uid} endpoint: {str(e)}", exc_info=True)
except HTTPException:
# Re-raise HTTP exceptions as they are
raise
except Exception as e:
# Log the full traceback for debugging
import traceback
error_trace = traceback.format_exc()
print(f"[ERROR] Error in /me/{uid} endpoint: {str(e)}\n{error_trace}")
# Return a 500 error with a generic message
raise HTTPException(status_code=500, detail="Internal server error")

View File

@ -1,3 +1,4 @@
{"uid":"devuser","size":22455090,"mtime":1747563720}
{"uid":"oib9","size":2019706,"mtime":1751124547}
{"uid":"devuser","size":65551721,"mtime":1752752391}
{"uid":"oib9","size":12735117,"mtime":1752843762}
{"uid":"oibchello","size":1549246,"mtime":1752840918}
{"uid":"orangeicebear","size":1734396,"mtime":1748767975}

View File

@ -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');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
// 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');
// 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);
// Let the browser handle external links
if (href && (href.startsWith('http') || href.startsWith('mailto:'))) {
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;
// If no data-target and no hash in href, let browser handle it
if (!target && (!href || !href.startsWith('#'))) {
return;
}
section.scrollIntoView({ behavior: "smooth" });
// Prefer data-target over href
let sectionId = target || (href ? href.substring(1) : '');
// Special case for the 'me' route which maps to 'me-page'
if (sectionId === 'me') {
sectionId = 'me-page';
}
// Skip if no valid section ID
if (!sectionId) {
console.warn('No valid section ID in navigation link:', link);
return;
}
// Let the router handle the navigation
e.preventDefault();
e.stopPropagation();
// Update body class to reflect the current section
document.body.className = ''; // Clear existing classes
document.body.classList.add(`page-${sectionId.replace('-page', '')}`);
// Update URL hash - the router will handle the rest
window.location.hash = sectionId;
// Close mobile menu if open
const burger = document.getElementById('burger-toggle');
if (burger && burger.checked) burger.checked = false;
};
// Add click event listeners to all navigation links
navLinks.forEach(link => {
link.addEventListener('click', handleNavClick);
});
// Handle initial page load and hash changes
const handleHashChange = () => {
let hash = window.location.hash.substring(1);
// Map URL hashes to section IDs if they don't match exactly
const sectionMap = {
'welcome': 'welcome-page',
'streams': 'stream-page',
'account': 'register-page',
'login': 'login-page',
'me': 'me-page'
};
// Use mapped section ID or the hash as is
const sectionId = sectionMap[hash] || hash || 'welcome-page';
const targetSection = document.getElementById(sectionId);
if (targetSection) {
// Hide all sections
document.querySelectorAll('main > section').forEach(section => {
section.hidden = section.id !== sectionId;
});
// Show target section
targetSection.hidden = false;
targetSection.scrollIntoView({ behavior: 'smooth' });
// Update active state of navigation links
navLinks.forEach(link => {
const linkHref = link.getAttribute('href');
// Match both the exact hash and the mapped section ID
link.classList.toggle('active',
linkHref === `#${hash}` ||
linkHref === `#${sectionId}` ||
link.getAttribute('data-target') === sectionId ||
link.getAttribute('data-target') === hash
);
});
// Special handling for streams page
if (sectionId === 'stream-page' && typeof window.maybeLoadStreamsOnShow === 'function') {
window.maybeLoadStreamsOnShow();
}
});
});
} else {
console.warn(`Section with ID '${sectionId}' not found`);
}
};
// Listen for hash changes
window.addEventListener('hashchange', handleHashChange);
// Handle initial page load
handleHashChange();
}
function initProfilePlayer() {
@ -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,9 +858,10 @@ 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;
@ -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
View 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
*/

View 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;
}
}

View File

@ -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;
}

View File

@ -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
View 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;
}

View File

@ -1,5 +1,6 @@
import { showToast } from "./toast.js";
// Utility function to get cookie value by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
@ -8,7 +9,7 @@ function getCookie(name) {
}
// dashboard.js — toggle guest vs. user dashboard and reposition streams link
// Logout function
// Global state
let isLoggingOut = false;
async function handleLogout(event) {
@ -31,8 +32,10 @@ async function handleLogout(event) {
localStorage.removeItem('confirmed_uid');
localStorage.removeItem('last_page');
// Clear cookie
document.cookie = 'uid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Clear session cookie with SameSite attribute to match how it was set
const isLocalhost = window.location.hostname === 'localhost';
const secureFlag = !isLocalhost ? '; Secure' : '';
document.cookie = `sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=Lax${secureFlag};`;
// Update UI state immediately
const userDashboard = document.getElementById('user-dashboard');
@ -134,6 +137,31 @@ async function handleDeleteAccount() {
}
}
// Debug function to check element visibility and styles
function debugElementVisibility(elementId) {
const el = document.getElementById(elementId);
if (!el) {
console.error(`[DEBUG] Element ${elementId} not found`);
return {};
}
const style = window.getComputedStyle(el);
return {
id: elementId,
exists: true,
display: style.display,
visibility: style.visibility,
opacity: style.opacity,
hidden: el.hidden,
classList: Array.from(el.classList),
parentDisplay: el.parentElement ? window.getComputedStyle(el.parentElement).display : 'no-parent',
parentVisibility: el.parentElement ? window.getComputedStyle(el.parentElement).visibility : 'no-parent',
rect: el.getBoundingClientRect()
};
}
/**
* Initialize the dashboard and handle authentication state
*/
async function initDashboard() {
console.log('[DASHBOARD] Initializing dashboard...');
@ -143,14 +171,7 @@ async function initDashboard() {
const userUpload = document.getElementById('user-upload-area');
const logoutButton = document.getElementById('logout-button');
const deleteAccountButton = document.getElementById('delete-account-button');
console.log('[DASHBOARD] Elements found:', {
guestDashboard: !!guestDashboard,
userDashboard: !!userDashboard,
userUpload: !!userUpload,
logoutButton: !!logoutButton,
deleteAccountButton: !!deleteAccountButton
});
const fileList = document.getElementById('file-list');
// Add click event listeners for logout and delete account buttons
if (logoutButton) {
@ -166,40 +187,145 @@ async function initDashboard() {
});
}
const uid = getCookie('uid');
console.log('[DASHBOARD] UID from cookie:', uid);
// Check authentication state - consolidated to avoid duplicate declarations
const hasAuthCookie = document.cookie.includes('isAuthenticated=true');
const hasUidCookie = document.cookie.includes('uid=');
const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true';
const hasAuthToken = localStorage.getItem('authToken') !== null;
const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken;
// Debug authentication state
console.log('[AUTH] Authentication state:', {
hasAuthCookie,
hasUidCookie,
hasLocalStorageAuth,
hasAuthToken,
isAuthenticated,
cookies: document.cookie,
localStorage: {
isAuthenticated: localStorage.getItem('isAuthenticated'),
uid: localStorage.getItem('uid'),
authToken: localStorage.getItem('authToken') ? 'present' : 'not present'
},
bodyClasses: document.body.className
});
// Handle authenticated user
if (isAuthenticated) {
console.log('[DASHBOARD] User is authenticated, showing user dashboard');
if (userDashboard) userDashboard.style.display = 'block';
if (userUpload) userUpload.style.display = 'block';
if (guestDashboard) guestDashboard.style.display = 'none';
// Add authenticated class to body if not present
document.body.classList.add('authenticated');
// Get UID from cookies or localStorage
let uid = getCookie('uid') || localStorage.getItem('uid');
// Guest view
if (!uid) {
console.log('[DASHBOARD] No UID found, showing guest dashboard');
console.warn('[DASHBOARD] No UID found in cookies or localStorage');
// Try to get UID from the URL or hash fragment
const urlParams = new URLSearchParams(window.location.search);
uid = urlParams.get('uid') || window.location.hash.substring(1);
if (uid) {
console.log(`[DASHBOARD] Using UID from URL/hash: ${uid}`);
localStorage.setItem('uid', uid);
} else {
console.error('[DASHBOARD] No UID available for file listing');
if (fileList) {
fileList.innerHTML = `
<li class="error-message">
Error: Could not determine user account. Please <a href="/#login" class="login-link">log in</a> again.
</li>`;
}
return;
}
}
// Initialize file listing if we have a UID
if (window.fetchAndDisplayFiles) {
console.log(`[DASHBOARD] Initializing file listing for UID: ${uid}`);
try {
await window.fetchAndDisplayFiles(uid);
} catch (error) {
console.error('[DASHBOARD] Error initializing file listing:', error);
if (fileList) {
fileList.innerHTML = `
<li class="error-message">
Error loading files: ${error.message || 'Unknown error'}.
Please <a href="/#login" class="login-link">log in</a> again.
</li>`;
}
}
}
} else {
// Guest view
console.log('[DASHBOARD] User not authenticated, showing guest dashboard');
if (guestDashboard) guestDashboard.style.display = 'block';
if (userDashboard) userDashboard.style.display = 'none';
if (userUpload) userUpload.style.display = 'none';
if (logoutButton) logoutButton.style.display = 'none';
if (deleteAccountButton) deleteAccountButton.style.display = 'none';
const mePage = document.getElementById('me-page');
if (mePage) mePage.style.display = 'none';
// Remove authenticated class if present
document.body.classList.remove('authenticated');
// Show login prompt
if (fileList) {
fileList.innerHTML = `
<li class="error-message">
Please <a href="/#login" class="login-link">log in</a> to view your files.
</li>`;
}
}
// Log authentication details for debugging
console.log('[DASHBOARD] Authentication details:', {
uid: getCookie('uid') || localStorage.getItem('uid'),
cookies: document.cookie,
localStorage: {
uid: localStorage.getItem('uid'),
isAuthenticated: localStorage.getItem('isAuthenticated'),
authToken: localStorage.getItem('authToken') ? 'present' : 'not present'
}
});
// If not authenticated, show guest view and return early
if (!isAuthenticated) {
console.log('[DASHBOARD] User not authenticated, showing guest dashboard');
if (guestDashboard) guestDashboard.style.display = 'block';
if (userDashboard) userDashboard.style.display = 'none';
if (userUpload) userUpload.style.display = 'none';
// Remove authenticated class if present
document.body.classList.remove('authenticated');
// Show login prompt
if (fileList) {
fileList.innerHTML = `
<li class="error-message">
Please <a href="/#login" class="login-link">log in</a> to view your files.
</li>`;
}
return;
}
// Logged-in view - show user dashboard by default
// Logged-in view - show user dashboard
console.log('[DASHBOARD] User is logged in, showing user dashboard');
// Log current display states
console.log('[DASHBOARD] Current display states:', {
// Get all page elements
const mePage = document.getElementById('me-page');
// Log current display states for debugging
console.log('[DASHBOARD] Updated display states:', {
guestDashboard: guestDashboard ? window.getComputedStyle(guestDashboard).display : 'not found',
userDashboard: userDashboard ? window.getComputedStyle(userDashboard).display : 'not found',
userUpload: userUpload ? window.getComputedStyle(userUpload).display : 'not found',
logoutButton: logoutButton ? window.getComputedStyle(logoutButton).display : 'not found',
deleteAccountButton: deleteAccountButton ? window.getComputedStyle(deleteAccountButton).display : 'not found'
deleteAccountButton: deleteAccountButton ? window.getComputedStyle(deleteAccountButton).display : 'not found',
mePage: mePage ? window.getComputedStyle(mePage).display : 'not found'
});
// Show delete account button for logged-in users
if (deleteAccountButton) {
deleteAccountButton.style.display = 'block';
console.log('[DASHBOARD] Showing delete account button');
}
// Hide guest dashboard
if (guestDashboard) {
console.log('[DASHBOARD] Hiding guest dashboard');
@ -213,6 +339,14 @@ async function initDashboard() {
userDashboard.style.visibility = 'visible';
userDashboard.hidden = false;
// Log final visibility state after changes
console.log('[DEBUG] Final visibility state after showing user dashboard:', {
userDashboard: debugElementVisibility('user-dashboard'),
guestDashboard: debugElementVisibility('guest-dashboard'),
computedDisplay: window.getComputedStyle(userDashboard).display,
computedVisibility: window.getComputedStyle(userDashboard).visibility
});
// Debug: Check if the element is actually in the DOM
console.log('[DASHBOARD] User dashboard parent:', userDashboard.parentElement);
console.log('[DASHBOARD] User dashboard computed display:', window.getComputedStyle(userDashboard).display);
@ -234,21 +368,28 @@ async function initDashboard() {
}
// Show me-page for logged-in users
const mePage = document.getElementById('me-page');
if (mePage) {
console.log('[DASHBOARD] Showing me-page');
mePage.style.display = 'block';
}
try {
// Try to get UID from various sources
let uid = getCookie('uid') || localStorage.getItem('uid');
// If we have a valid UID, try to fetch user data
if (uid && uid !== 'welcome-page' && uid !== 'undefined' && uid !== 'null') {
console.log('[DASHBOARD] Found valid UID:', uid);
console.log(`[DEBUG] Fetching user data for UID: ${uid}`);
const res = await fetch(`/me/${uid}`);
if (!res.ok) {
const errorText = await res.text();
console.error(`[ERROR] Failed to fetch user data: ${res.status} ${res.statusText}`, errorText);
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const response = await fetch(`/me/${uid}`);
if (!response.ok) {
const errorText = await response.text();
console.error(`[ERROR] Failed to fetch user data: ${response.status} ${response.statusText}`, errorText);
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await res.json();
// Parse and handle the response data
const data = await response.json();
console.log('[DEBUG] User data loaded:', data);
// Ensure upload area is visible if last_page was me-page
@ -259,6 +400,7 @@ async function initDashboard() {
// Remove guest warning if present
const guestMsg = document.getElementById('guest-warning-msg');
if (guestMsg && guestMsg.parentNode) guestMsg.parentNode.removeChild(guestMsg);
// Show user dashboard and logout button
if (userDashboard) userDashboard.style.display = '';
if (logoutButton) {
@ -268,22 +410,33 @@ async function initDashboard() {
// Set audio source
const meAudio = document.getElementById('me-audio');
if (meAudio && data && data.username) {
// Use username instead of UID for the audio file path
meAudio.src = `/audio/${encodeURIComponent(data.username)}/stream.opus?t=${Date.now()}`;
const username = data?.username || '';
if (meAudio) {
if (username) {
// Use username for the audio file path if available
meAudio.src = `/audio/${encodeURIComponent(username)}/stream.opus?t=${Date.now()}`;
console.log('Setting audio source to:', meAudio.src);
} else if (meAudio && uid) {
} else if (uid) {
// Fallback to UID if username is not available
meAudio.src = `/audio/${encodeURIComponent(uid)}/stream.opus?t=${Date.now()}`;
console.warn('Using UID fallback for audio source:', meAudio.src);
}
}
// Update quota and ensure quota meter is visible
// Update quota and ensure quota meter is visible if data is available
const quotaMeter = document.getElementById('quota-meter');
const quotaBar = document.getElementById('quota-bar');
const quotaText = document.getElementById('quota-text');
if (quotaBar) quotaBar.value = data.quota;
if (quotaText) quotaText.textContent = `${data.quota} MB used`;
if (quotaBar && data.quota !== undefined) {
quotaBar.value = data.quota;
}
if (quotaText && data.quota !== undefined) {
quotaText.textContent = `${data.quota} MB`;
}
if (quotaMeter) {
quotaMeter.hidden = false;
quotaMeter.style.display = 'block'; // Ensure it's not hidden by display:none
@ -291,16 +444,39 @@ async function initDashboard() {
// Fetch and display the list of uploaded files if the function is available
if (window.fetchAndDisplayFiles) {
window.fetchAndDisplayFiles(uid);
console.log('[DASHBOARD] Calling fetchAndDisplayFiles with UID:', uid);
// Ensure we have the most up-to-date UID from the response data if available
const effectiveUid = data?.uid || uid;
console.log('[DASHBOARD] Using effective UID:', effectiveUid);
window.fetchAndDisplayFiles(effectiveUid);
} else {
console.error('[DASHBOARD] fetchAndDisplayFiles function not found!');
}
} else {
// No valid UID found, ensure we're in guest mode
console.log('[DASHBOARD] No valid UID found, showing guest dashboard');
userDashboard.style.display = 'none';
guestDashboard.style.display = 'block';
userUpload.style.display = 'none';
document.body.classList.remove('authenticated');
return; // Exit early for guest users
}
// Ensure Streams link remains in nav, not moved
// (No action needed if static)
} catch (e) {
console.warn('Dashboard init error, treating as guest:', e);
console.warn('Dashboard init error, falling back to guest mode:', e);
userUpload.style.display = '';
// Ensure guest UI is shown
userUpload.style.display = 'none';
userDashboard.style.display = 'none';
if (guestDashboard) guestDashboard.style.display = 'block';
// Update body classes
document.body.classList.remove('authenticated');
document.body.classList.add('guest-mode');
// Ensure navigation is in correct state
const registerLink = document.getElementById('guest-login');
const streamsLink = document.getElementById('guest-streams');
if (registerLink && streamsLink) {
@ -309,45 +485,417 @@ async function initDashboard() {
}
}
document.addEventListener('DOMContentLoaded', initDashboard);
// Function to delete a file - only define if not already defined
if (typeof window.deleteFile === 'undefined') {
window.deleteFile = async function(uid, fileName, listItem) {
if (!confirm(`Are you sure you want to delete "${fileName}"? This cannot be undone.`)) {
return;
}
// Registration form handler for guests
// Handles the submit event on #register-form, sends data to /register, and alerts the user with the result
try {
console.log(`[FILES] Deleting file: ${fileName} for user: ${uid}`);
const response = await fetch(`/delete-file/${uid}/${encodeURIComponent(fileName)}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
console.log('[FILES] Delete response:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error('[FILES] Delete error:', errorText);
showToast(`Error deleting file: ${response.statusText}`, 'error');
return;
}
// Remove the file from the UI
if (listItem && listItem.parentNode) {
listItem.parentNode.removeChild(listItem);
}
showToast('File deleted successfully', 'success');
// If no files left, show "no files" message
const fileList = document.getElementById('file-list');
if (fileList && fileList.children.length === 0) {
fileList.innerHTML = '<li class="no-files">No files uploaded yet.</li>';
}
} catch (error) {
console.error('[FILES] Error deleting file:', error);
showToast('Error deleting file. Please try again.', 'error');
}
}; // Close the function expression
} // Close the if statement
// Function to fetch and display user's uploaded files
async function fetchAndDisplayFiles(uid) {
const fileList = document.getElementById('file-list');
if (!fileList) {
console.error('[FILES] File list element not found');
return;
}
console.log(`[FILES] Fetching files for user: ${uid}`);
fileList.innerHTML = '<li class="loading-message">Loading your files...</li>';
// Prepare headers with auth token if available
const authToken = localStorage.getItem('authToken');
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
console.log('[FILES] Making request to /me with headers:', headers);
try {
// The backend should handle authentication via session cookies
// We include the auth token in headers if available, but don't rely on it for auth
console.log('[FILES] Making request to /me with credentials...');
const response = await fetch('/me', {
method: 'GET',
credentials: 'include', // Important: include cookies for session auth
headers: headers
});
console.log('[FILES] Response status:', response.status);
console.log('[FILES] Response headers:', Object.fromEntries([...response.headers.entries()]));
// Get response as text first to handle potential JSON parsing errors
const responseText = await response.text();
console.log('[FILES] Raw response text:', responseText);
// Parse the JSON response
let responseData = {};
if (responseText && responseText.trim() !== '') {
try {
responseData = JSON.parse(responseText);
console.log('[FILES] Successfully parsed JSON response:', responseData);
} catch (e) {
console.error('[FILES] Failed to parse JSON response. Response text:', responseText);
console.error('[FILES] Error details:', e);
// If we have a non-JSON response but the status is 200, try to handle it
if (response.ok) {
console.warn('[FILES] Non-JSON response with 200 status, treating as empty response');
} else {
throw new Error(`Invalid JSON response from server: ${e.message}`);
}
}
} else {
console.log('[FILES] Empty response received, using empty object');
}
// Note: Authentication is handled by the parent component
// We'll just handle the response status without clearing auth state
if (response.ok) {
// Check if the response has the expected format
if (!responseData || !Array.isArray(responseData.files)) {
console.error('[FILES] Invalid response format, expected {files: [...]}:', responseData);
fileList.innerHTML = '<li>Error: Invalid response from server</li>';
return;
}
const files = responseData.files;
console.log('[FILES] Files array:', files);
if (files.length === 0) {
fileList.innerHTML = '<li class="no-files">No files uploaded yet.</li>';
return;
}
// Clear the loading message
fileList.innerHTML = '';
// Add each file to the list
files.forEach(fileName => {
const fileExt = fileName.split('.').pop().toLowerCase();
const fileUrl = `/data/${uid}/${encodeURIComponent(fileName)}`;
const fileSize = 'N/A'; // We don't have size info in the current API response
const listItem = document.createElement('li');
listItem.className = 'file-item';
// Create file icon based on file extension
let fileIcon = '📄'; // Default icon
if (['mp3', 'wav', 'ogg', 'm4a', 'opus'].includes(fileExt)) {
fileIcon = '🎵';
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt)) {
fileIcon = '🖼️';
} else if (['pdf', 'doc', 'docx', 'txt'].includes(fileExt)) {
fileIcon = '📄';
}
listItem.innerHTML = `
<div class="file-info">
<span class="file-icon">${fileIcon}</span>
<a href="${fileUrl}" class="file-name" target="_blank" rel="noopener noreferrer">
${fileName}
</a>
<span class="file-size">${fileSize}</span>
</div>
<div class="file-actions">
<a href="${fileUrl}" class="download-button" download>
<span class="button-icon">⬇️</span>
<span class="button-text">Download</span>
</a>
<button class="delete-button" data-filename="${fileName}">
<span class="button-icon">🗑️</span>
<span class="button-text">Delete</span>
</button>
</div>
`;
// Add delete button handler
const deleteButton = listItem.querySelector('.delete-button');
if (deleteButton) {
deleteButton.addEventListener('click', () => deleteFile(uid, fileName, listItem));
}
fileList.appendChild(listItem);
});
} else {
// Handle non-OK responses
if (response.status === 401) {
// Parent component will handle authentication state
fileList.innerHTML = `
<li class="error-message">
Please <a href="/#login" class="login-link">log in</a> to view your files.
</li>`;
} else {
fileList.innerHTML = `
<li class="error-message">
Error loading files (${response.status}). Please try again later.
</li>`;
}
console.error('[FILES] Server error:', response.status, response.statusText);
}
} catch (error) {
console.error('[FILES] Error fetching files:', error);
const fileList = document.getElementById('file-list');
if (fileList) {
fileList.innerHTML = `
<li class="error-message">
Error loading files: ${error.message || 'Unknown error'}
</li>`;
}
}
}
// Function to handle file deletion
async function deleteFile(fileId, uid) {
if (!confirm('Are you sure you want to delete this file?')) {
return;
}
try {
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Refresh the file list
fetchAndDisplayFiles(uid);
showToast('File deleted successfully');
} catch (error) {
console.error('[FILES] Error deleting file:', error);
showToast('Error deleting file', 'error');
}
}
// Initialize file upload functionality
function initFileUpload() {
const uploadArea = document.getElementById('user-upload-area');
const fileInput = document.getElementById('fileInputUser');
if (!uploadArea || !fileInput) {
console.warn('[UPLOAD] Required elements not found for file upload');
return;
}
// Handle click on upload area
uploadArea.addEventListener('click', () => {
fileInput.click();
});
// Handle file selection
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
// Check file size (100MB limit)
if (file.size > 100 * 1024 * 1024) {
showToast('File is too large. Maximum size is 100MB.', 'error');
return;
}
// Show loading state
const originalText = uploadArea.innerHTML;
uploadArea.innerHTML = 'Uploading...';
try {
const formData = new FormData();
formData.append('file', file);
// Get UID from localStorage (parent UI ensures we're authenticated)
const uid = localStorage.getItem('uid');
formData.append('uid', uid);
// Proceed with the upload
const response = await fetch('/upload', {
method: 'POST',
body: formData,
credentials: 'include', // Include cookies for authentication
headers: {
'Accept': 'application/json' // Explicitly accept JSON response
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || 'Upload failed');
}
const result = await response.json();
showToast('File uploaded successfully!');
// Refresh file list
if (window.fetchAndDisplayFiles) {
window.fetchAndDisplayFiles(uid);
}
} catch (error) {
console.error('[UPLOAD] Error uploading file:', error);
showToast(`Upload failed: ${error.message}`, 'error');
} finally {
// Reset file input and restore upload area text
fileInput.value = '';
uploadArea.innerHTML = originalText;
}
});
// Handle drag and drop
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
uploadArea.classList.add('highlight');
}
function unhighlight() {
uploadArea.classList.remove('highlight');
}
// Handle dropped files
uploadArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
fileInput.files = files;
const event = new Event('change');
fileInput.dispatchEvent(event);
}
});
}
// Main initialization when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', () => {
// Initialize dashboard components
initDashboard();
initFileUpload();
// Make fetchAndDisplayFiles available globally
window.fetchAndDisplayFiles = fetchAndDisplayFiles;
// Login/Register (guest)
const regForm = document.getElementById('register-form');
if (regForm) {
regForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(regForm);
const submitButton = regForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.textContent;
try {
// Disable button during submission
submitButton.disabled = true;
submitButton.textContent = 'Sending...';
const res = await fetch('/register', {
method: 'POST',
body: formData
body: formData,
headers: {
'Accept': 'application/json'
}
});
let data;
const contentType = res.headers.get('content-type');
try {
if (contentType && contentType.includes('application/json')) {
data = await res.json();
} else {
data = { detail: await res.text() };
const text = await res.text();
data = { detail: text };
}
if (res.ok) {
showToast('Confirmation sent! Check your email.');
showToast('', 'success');
// Clear the form on success
regForm.reset();
} else {
showToast('Registration failed: ' + (data.detail || res.status));
showToast(`Error: ${data.detail || 'Unknown error occurred'}`, 'error');
console.error('Registration failed:', data);
}
} catch (parseError) {
console.error('Error parsing response:', parseError);
showToast('Error processing the response. Please try again.', 'error');
}
} catch (err) {
showToast('Network error: ' + err);
console.error('Network error:', err);
showToast('Network error. Please check your connection and try again.', 'error');
} finally {
// Re-enable button
submitButton.disabled = false;
submitButton.textContent = originalButtonText;
}
});
}
});
// Connect Login or Register link to register form
document.addEventListener('DOMContentLoaded', () => {
// Connect Login or Register link to register form
// Login/Register (guest)
const loginLink = document.getElementById('guest-login');
if (loginLink) {
@ -417,4 +965,154 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
});
});
// Back to top button functionality
const backToTop = document.getElementById('back-to-top');
if (backToTop) {
backToTop.addEventListener('click', (e) => {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
// Mobile menu functionality
const menuToggle = document.getElementById('mobile-menu-toggle');
const mainNav = document.getElementById('main-navigation');
if (menuToggle && mainNav) {
// Toggle mobile menu
menuToggle.addEventListener('click', () => {
const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true' || false;
menuToggle.setAttribute('aria-expanded', !isExpanded);
mainNav.setAttribute('aria-hidden', isExpanded);
// Toggle mobile menu visibility
if (isExpanded) {
mainNav.classList.remove('mobile-visible');
document.body.style.overflow = '';
} else {
mainNav.classList.add('mobile-visible');
document.body.style.overflow = 'hidden';
}
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
const isClickInsideNav = mainNav.contains(e.target);
const isClickOnToggle = menuToggle === e.target || menuToggle.contains(e.target);
if (mainNav.classList.contains('mobile-visible') && !isClickInsideNav && !isClickOnToggle) {
mainNav.classList.remove('mobile-visible');
menuToggle.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
}
});
}
// Handle navigation link clicks
const navLinks = document.querySelectorAll('nav a[href^="#"]');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
const targetId = link.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
e.preventDefault();
// Close mobile menu if open
if (mainNav && mainNav.classList.contains('mobile-visible')) {
mainNav.classList.remove('mobile-visible');
if (menuToggle) {
menuToggle.setAttribute('aria-expanded', 'false');
}
document.body.style.overflow = '';
}
// Smooth scroll to target
targetElement.scrollIntoView({ behavior: 'smooth' });
// Update URL without page reload
if (history.pushState) {
history.pushState(null, '', targetId);
} else {
location.hash = targetId;
}
}
});
});
// Helper function to handle page section navigation
const setupPageNavigation = (linkIds, pageId) => {
const links = linkIds
.map(id => document.getElementById(id))
.filter(Boolean);
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
document.querySelectorAll('main > section').forEach(sec => {
sec.hidden = sec.id !== pageId;
});
const targetPage = document.getElementById(pageId);
if (targetPage) {
targetPage.hidden = false;
targetPage.scrollIntoView({ behavior: 'smooth' });
}
});
});
};
// Setup navigation for different sections
setupPageNavigation(['guest-terms', 'user-terms'], 'terms-page');
setupPageNavigation(['guest-imprint', 'user-imprint'], 'imprint-page');
setupPageNavigation(['guest-privacy', 'user-privacy'], 'privacy-page');
// Registration form handler for guests - removed duplicate declaration
// The form submission is already handled earlier in the file
// Login link handler - removed duplicate declaration
// The login link is already handled by the setupPageNavigation function
// Handle drag and drop
const uploadArea = document.getElementById('upload-area');
if (uploadArea) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, unhighlight, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight() {
uploadArea.classList.add('highlight');
}
function unhighlight() {
uploadArea.classList.remove('highlight');
}
// Handle dropped files
uploadArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
const fileInput = document.getElementById('file-input');
fileInput.files = files;
const event = new Event('change');
fileInput.dispatchEvent(event);
}
});
}
}); // End of DOMContentLoaded

View File

@ -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;
}
/* Section styles are now handled in style.css */
/* 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;
}
/* 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;
}
}

View File

@ -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>
<section id="privacy-page" class="always-visible">
<div>
<h2>Privacy Policy</h2>
<article>
</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 | 48kHz | 60kbps)</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>Youll 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 | 48kHz | 60kbps</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>

View 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);
});

View File

@ -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;
// Function to set up guest navigation links
function setupGuestNav() {
const guestDashboard = document.getElementById('guest-dashboard');
if (!guestDashboard) return;
// 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 = '';
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) => {
const links = guestDashboard.querySelectorAll('a');
links.forEach(link => {
link.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) => {
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();
}
});
});
}
// Function to set up user navigation links
function setupUserNav() {
const userDashboard = document.getElementById('user-dashboard');
if (!userDashboard) return;
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);
}
}
});
li.appendChild(a);
navList.appendChild(li);
}
});
nav.appendChild(navList);
return nav;
}
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.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
// Set up menu links and active state
setupMenuLinks();
updateActiveNav();
// 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;
}
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;
// Set up guest and user navigation links
setupGuestNav();
setupUserNav();
const target = link.getAttribute('data-target');
if (target) {
e.preventDefault();
console.log('Navigation link clicked:', target);
showOnly(target);
closeMenu();
// Handle hash changes for SPA navigation
window.addEventListener('hashchange', updateActiveNav);
}
// Update active state
document.querySelectorAll('.nav-link').forEach(l => {
l.classList.remove('active');
});
// 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;

View File

@ -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!');

View File

@ -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 */

View File

@ -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');
// Update navigation active states
this.updateActiveNav(validId);
},
// Handle all sections
this.sections.forEach(sec => {
// Skip quota meter as it's already handled
if (sec.id === 'quota-meter') return;
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');
}
});
}
};
// 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;
// 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;
}
// 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);
const currentHash = window.location.hash.substring(1);
const isLegalPage = ['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId);
sec.hidden = !shouldShow;
sec.tabIndex = shouldShow ? 0 : -1;
// 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 = (id === "me-page" && uid) ? '' : 'none';
userUpload.style.display = (window.location.hash === '#me-page' && uid) ? '' : 'none';
}
localStorage.setItem("last_page", id);
const target = document.getElementById(id);
if (target) target.focus();
},
init() {
// 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();
}
};
const showOnly = Router.showOnly.bind(Router);
// 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();
});

View File

@ -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);

View File

@ -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...');
controller.abort();
loadAndRenderStreams();
// 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();
}
}, 10000); // 10 second timeout
// Clean up and retry with backoff
controller.abort();
// 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');
}
}
}, 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) {
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>
`;
let errorMessage = 'Error loading streams. ';
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,14 +744,18 @@ function cleanupAudio() {
}
}
// Event delegation for play/pause buttons
document.addEventListener('click', async (e) => {
// 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');
if (!playPauseBtn) return;
// Skip if not a play button or if it's the personal stream's play button
if (!playPauseBtn || playPauseBtn.closest('#me-page')) return;
// Prevent default to avoid any potential form submission or link following
e.preventDefault();
// Prevent event from bubbling up to document-level handlers
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
const uid = playPauseBtn.dataset.uid;
if (!uid) {
@ -747,7 +791,8 @@ document.addEventListener('click', async (e) => {
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) => {

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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;

View File

@ -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}">
<li class="file-item" data-filename="${file.name}">
<div class="file-name" 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>
</div>
<span style="color: #888; white-space: nowrap; margin-left: 10px;">${sizeMB} MB</span>
${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 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;

View File

@ -6,15 +6,17 @@ from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from pathlib import Path
from convert_to_opus import convert_to_opus
from database import get_db
from models import UploadLog, UserQuota, User
from sqlalchemy import select
from database import get_db
limiter = Limiter(key_func=get_remote_address)
router = APIRouter()
# # Not needed for SlowAPI ≥0.1.5
DATA_ROOT = Path("./data")
@limiter.limit("5/minute")
@router.post("/upload")
async def upload(request: Request, db = Depends(get_db), uid: str = Form(...), file: UploadFile = Form(...)):