Files
2025-05-21 08:58:07 +02:00

235 lines
8.2 KiB
JavaScript

// static/streams-ui.js — public streams loader and profile-link handling
import { showOnly } from './router.js';
// Removed loadingStreams and lastStreamsPageVisible guards for instant fetch
export function initStreamsUI() {
initStreamLinks();
window.addEventListener('popstate', () => {
highlightActiveProfileLink();
maybeLoadStreamsOnShow();
});
document.addEventListener('visibilitychange', maybeLoadStreamsOnShow);
maybeLoadStreamsOnShow();
}
function maybeLoadStreamsOnShow() {
// Expose globally for nav.js
const streamPage = document.getElementById('stream-page');
const isVisible = streamPage && !streamPage.hidden;
if (isVisible) {
loadAndRenderStreams();
}
}
window.maybeLoadStreamsOnShow = maybeLoadStreamsOnShow;
let currentlyPlayingAudio = null;
let currentlyPlayingButton = null;
document.addEventListener('DOMContentLoaded', initStreamsUI);
function loadAndRenderStreams() {
const ul = document.getElementById('stream-list');
if (!ul) {
console.warn('[streams-ui] #stream-list not found in DOM');
return;
}
console.debug('[streams-ui] loadAndRenderStreams (SSE mode) called');
ul.innerHTML = '<li>Loading...</li>';
let gotAny = false;
let streams = [];
// Close previous EventSource if any
if (window._streamsSSE) {
window._streamsSSE.close();
}
const evtSource = new window.EventSource('/streams-sse');
window._streamsSSE = evtSource;
evtSource.onmessage = function(event) {
console.debug('[streams-ui] SSE event received:', event.data);
try {
const data = JSON.parse(event.data);
if (data.end) {
if (!gotAny) {
ul.innerHTML = '<li>No active streams.</li>';
}
evtSource.close();
highlightActiveProfileLink();
return;
}
// Remove Loading... on any valid event
if (!gotAny) {
ul.innerHTML = '';
gotAny = true;
}
streams.push(data);
const uid = data.uid || '';
const sizeMb = data.size ? (data.size / (1024 * 1024)).toFixed(1) : '?';
const mtime = data.mtime ? new Date(data.mtime * 1000).toISOString().split('T')[0].replace(/-/g, '/') : '';
const li = document.createElement('li');
li.innerHTML = `
<article class="stream-player">
<h3>${uid}</h3>
<audio id="audio-${uid}" class="stream-audio" preload="auto" crossOrigin="anonymous" src="/audio/${encodeURIComponent(uid)}/stream.opus"></audio>
<div class="audio-controls">
<button id="play-pause-${uid}">▶</button>
</div>
<p class="stream-info" style='color:gray;font-size:90%'>[${sizeMb} MB, ${mtime}]</p>
</article>
`;
// Add play/pause handler after appending to DOM
ul.appendChild(li);
// Wait for DOM update
requestAnimationFrame(() => {
const playPauseButton = document.getElementById(`play-pause-${uid}`);
const audio = document.getElementById(`audio-${uid}`);
if (playPauseButton && audio) {
playPauseButton.addEventListener('click', () => {
try {
if (audio.paused) {
// Stop any currently playing audio first
if (currentlyPlayingAudio && currentlyPlayingAudio !== audio) {
currentlyPlayingAudio.pause();
if (currentlyPlayingButton) {
currentlyPlayingButton.textContent = '▶';
}
}
// Stop the main player if it's playing
if (typeof window.stopMainAudio === 'function') {
window.stopMainAudio();
}
audio.play().then(() => {
playPauseButton.textContent = '⏸️';
currentlyPlayingAudio = audio;
currentlyPlayingButton = playPauseButton;
}).catch(e => {
console.error('Play failed:', e);
// Reset button if play fails
playPauseButton.textContent = '▶';
currentlyPlayingAudio = null;
currentlyPlayingButton = null;
});
} else {
audio.pause();
playPauseButton.textContent = '▶';
if (currentlyPlayingAudio === audio) {
currentlyPlayingAudio = null;
currentlyPlayingButton = null;
}
}
} catch (e) {
console.error('Audio error:', e);
playPauseButton.textContent = '▶';
if (currentlyPlayingAudio === audio) {
currentlyPlayingAudio = null;
currentlyPlayingButton = null;
}
}
});
}
});
highlightActiveProfileLink();
ul.appendChild(li);
highlightActiveProfileLink();
} catch (e) {
// Remove Loading... even if JSON parse fails, to avoid stuck UI
if (!gotAny) {
ul.innerHTML = '';
gotAny = true;
}
console.error('[streams-ui] SSE parse error', e, event.data);
}
};
evtSource.onerror = function(err) {
console.error('[streams-ui] SSE error', err);
ul.innerHTML = '<li>Error loading stream list</li>';
if (typeof showToast === 'function') {
showToast('❌ Error loading public streams.');
}
evtSource.close();
// Add reload button if not present
const reloadButton = document.getElementById('reload-streams');
if (!reloadButton) {
const reloadHtml = '<button id="reload-streams" onclick="loadAndRenderStreams()">Reload</button>';
ul.insertAdjacentHTML('beforeend', reloadHtml);
}
};
}
export function renderStreamList(streams) {
const ul = document.getElementById('stream-list');
if (!ul) {
console.warn('[streams-ui] renderStreamList: #stream-list not found');
return;
}
console.debug('[streams-ui] Rendering stream list:', streams);
if (Array.isArray(streams)) {
if (streams.length) {
// Sort by mtime descending (most recent first)
streams.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
ul.innerHTML = streams
.map(stream => {
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>`;
})
.join('');
} else {
ul.innerHTML = '<li>No active streams.</li>';
}
} else {
ul.innerHTML = '<li>Error: Invalid stream data.</li>';
console.error('[streams-ui] renderStreamList: streams is not an array', streams);
}
highlightActiveProfileLink();
console.debug('[streams-ui] renderStreamList complete');
}
export function highlightActiveProfileLink() {
const params = new URLSearchParams(window.location.search);
const profileUid = params.get('profile');
const ul = document.getElementById('stream-list');
if (!ul) return;
ul.querySelectorAll('a.profile-link').forEach(link => {
const url = new URL(link.href, window.location.origin);
const uidParam = url.searchParams.get('profile');
link.classList.toggle('active', uidParam === profileUid);
});
}
export function initStreamLinks() {
const ul = document.getElementById('stream-list');
if (!ul) return;
ul.addEventListener('click', e => {
const a = e.target.closest('a.profile-link');
if (!a || !ul.contains(a)) return;
e.preventDefault();
const url = new URL(a.href, window.location.origin);
const profileUid = url.searchParams.get('profile');
if (profileUid) {
if (window.location.search !== `?profile=${encodeURIComponent(profileUid)}`) {
window.profileNavigationTriggered = true;
window.history.pushState({}, '', `/?profile=${encodeURIComponent(profileUid)}#`);
window.dispatchEvent(new Event('popstate'));
} else {
// If already on this profile, still highlight
if (typeof window.highlightActiveProfileLink === "function") {
window.highlightActiveProfileLink();
}
}
}
});
}