feat: Overhaul client-side navigation and clean up project

- Implement a unified SPA routing system in nav.js, removing all legacy and conflicting navigation scripts (router.js, inject-nav.js, fix-nav.js).
- Refactor dashboard.js to delegate all navigation handling to the new nav.js module.
- Create new modular JS files (auth.js, personal-player.js, logger.js) to improve code organization.
- Fix all navigation-related bugs, including guest access and broken footer links.
- Clean up the project root by moving development scripts and backups to a dedicated /dev directory.
- Add a .gitignore file to exclude the database, logs, and other transient files from the repository.
This commit is contained in:
oib
2025-07-28 16:42:46 +02:00
parent 88e468b716
commit d497492186
34 changed files with 1279 additions and 3810 deletions

140
static/personal-player.js Normal file
View File

@ -0,0 +1,140 @@
import { showToast } from "./toast.js";
import { globalAudioManager } from './global-audio-manager.js';
// Module-level state for the personal player
let audio = null;
/**
* Finds or creates the audio element for the personal stream.
* @returns {HTMLAudioElement | null}
*/
function getOrCreateAudioElement() {
if (audio) {
return audio;
}
audio = document.createElement('audio');
audio.id = 'me-audio';
audio.preload = 'metadata';
audio.crossOrigin = 'use-credentials';
document.body.appendChild(audio);
// --- Setup Event Listeners (only once) ---
audio.addEventListener('error', (e) => {
console.error('Personal Player: Audio Element Error', e);
const error = audio.error;
let errorMessage = 'An unknown audio error occurred.';
if (error) {
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
errorMessage = 'Audio playback was aborted.';
break;
case error.MEDIA_ERR_NETWORK:
errorMessage = 'A network error caused the audio to fail.';
break;
case error.MEDIA_ERR_DECODE:
errorMessage = 'The audio could not be decoded.';
break;
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
errorMessage = 'The audio format is not supported by your browser.';
break;
default:
errorMessage = `An unexpected error occurred (Code: ${error.code}).`;
break;
}
}
showToast(errorMessage, 'error');
});
audio.addEventListener('play', () => updatePlayPauseButton(true));
audio.addEventListener('pause', () => updatePlayPauseButton(false));
audio.addEventListener('ended', () => updatePlayPauseButton(false));
// The canplaythrough listener is removed as it violates autoplay policies.
// The user will perform a second click to play the media after it's loaded.
return audio;
}
/**
* Updates the play/pause button icon based on audio state.
* @param {boolean} isPlaying - Whether the audio is currently playing.
*/
function updatePlayPauseButton(isPlaying) {
const playPauseBtn = document.querySelector('#me-page .play-pause-btn');
if (playPauseBtn) {
playPauseBtn.textContent = isPlaying ? '⏸️' : '▶️';
}
}
/**
* Loads the user's personal audio stream into the player.
* @param {string} uid - The user's unique ID.
*/
export async function loadProfileStream(uid) {
const audioElement = getOrCreateAudioElement();
const audioSrc = `/audio/${uid}/stream.opus?t=${Date.now()}`;
console.log(`[personal-player.js] Setting personal audio source to: ${audioSrc}`);
audioElement.src = audioSrc;
}
/**
* Initializes the personal audio player, setting up event listeners.
*/
export function initPersonalPlayer() {
const mePageSection = document.getElementById('me-page');
if (!mePageSection) return;
// Use a delegated event listener for the play button
mePageSection.addEventListener('click', (e) => {
const playPauseBtn = e.target.closest('.play-pause-btn');
if (!playPauseBtn) return;
e.stopPropagation();
const audio = getOrCreateAudioElement();
if (!audio) return;
try {
if (audio.paused) {
if (!audio.src || audio.src.endsWith('/#')) {
showToast('No audio file available. Please upload one first.', 'info');
return;
}
console.log('Attempting to play...');
globalAudioManager.startPlayback('personal', localStorage.getItem('uid') || 'personal');
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
console.error(`Initial play() failed: ${error.name}. This is expected on first load.`);
// If play fails, it's because the content isn't loaded.
// The recovery is to call load(). The user will need to click play again.
console.log('Calling load() to fetch media...');
audio.load();
showToast('Stream is loading. Please click play again in a moment.', 'info');
});
}
} else {
console.log('Attempting to pause...');
audio.pause();
}
} catch (err) {
console.error('A synchronous error occurred in handlePlayPause:', err);
showToast('An unexpected error occurred with the audio player.', 'error');
}
});
// Listen for stop requests from the global manager
globalAudioManager.addListener('personal', () => {
console.log('[personal-player.js] Received stop request from global audio manager.');
const audio = getOrCreateAudioElement();
if (audio && !audio.paused) {
console.log('[personal-player.js] Pausing personal audio player.');
audio.pause();
}
});
// Initial setup
getOrCreateAudioElement();
}