This commit is contained in:
commit
ae3224c18b
30 changed files with 2455 additions and 0 deletions
226
public/js/index.js
Normal file
226
public/js/index.js
Normal file
|
@ -0,0 +1,226 @@
|
|||
class StreamDashboard {
|
||||
constructor() {
|
||||
this.streams = [];
|
||||
this.filteredStreams = [];
|
||||
this.searchTimeout = null;
|
||||
this.refreshInterval = null;
|
||||
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
this.loadStreams();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.elements = {
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
totalStreams: document.getElementById("totalStreams"),
|
||||
onlineStreams: document.getElementById("onlineStreams"),
|
||||
loadingState: document.getElementById("loadingState"),
|
||||
errorState: document.getElementById("errorState"),
|
||||
streamsContainer: document.getElementById("streamsContainer"),
|
||||
noResults: document.getElementById("noResults"),
|
||||
retryBtn: document.getElementById("retryBtn"),
|
||||
};
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.elements.searchInput.addEventListener("input", (e) => {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.filterStreams(e.target.value);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
this.elements.retryBtn.addEventListener("click", () => {
|
||||
this.loadStreams();
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
this.stopAutoRefresh();
|
||||
} else {
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadStreams(true);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async loadStreams(silentRefresh = false) {
|
||||
if (!silentRefresh) {
|
||||
this.showLoading();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/streams");
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to load streams");
|
||||
}
|
||||
|
||||
const allStreams = result.data.streams || [];
|
||||
this.streams = allStreams.filter(
|
||||
(stream) => stream.publish?.active === true,
|
||||
);
|
||||
this.filteredStreams = [...this.streams];
|
||||
|
||||
this.updateStats();
|
||||
this.renderStreams();
|
||||
|
||||
if (!silentRefresh) {
|
||||
this.showStreams();
|
||||
}
|
||||
} catch {
|
||||
if (!silentRefresh) {
|
||||
this.showError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterStreams(searchTerm) {
|
||||
const term = searchTerm.toLowerCase().trim();
|
||||
|
||||
if (!term) {
|
||||
this.filteredStreams = [...this.streams];
|
||||
} else {
|
||||
this.filteredStreams = this.streams.filter(
|
||||
(stream) =>
|
||||
stream.name?.toLowerCase().includes(term) ||
|
||||
stream.url?.toLowerCase().includes(term) ||
|
||||
stream.app?.toLowerCase().includes(term),
|
||||
);
|
||||
}
|
||||
|
||||
this.renderStreams();
|
||||
|
||||
if (this.filteredStreams.length === 0 && term) {
|
||||
this.showNoResults();
|
||||
} else {
|
||||
this.showStreams();
|
||||
}
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
const total = this.streams.length;
|
||||
|
||||
this.elements.totalStreams.textContent = total;
|
||||
this.elements.onlineStreams.textContent = total;
|
||||
}
|
||||
|
||||
renderStreams() {
|
||||
this.elements.streamsContainer.innerHTML = "";
|
||||
|
||||
for (const stream of this.filteredStreams) {
|
||||
const streamCard = this.createStreamCard(stream);
|
||||
this.elements.streamsContainer.appendChild(streamCard);
|
||||
}
|
||||
}
|
||||
|
||||
createStreamCard(stream) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "stream-card";
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (!bytes) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const watchName = stream.name?.replace(/\/live\//, "/");
|
||||
const watchUrl = watchName
|
||||
? `${window.location.origin}/watch/${encodeURIComponent(watchName)}`
|
||||
: "Unknown Stream";
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="stream-header">
|
||||
<h3 class="stream-name">${this.escapeHtml(stream.name || "Unknown Stream")}</h3>
|
||||
</div>
|
||||
|
||||
<div class="stream-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Clients:</span>
|
||||
<span class="info-value">${stream.clients || 0}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Bitrate:</span>
|
||||
<span class="info-value">${formatBytes((stream.kbps?.recv_30s || 0) * 1024)}/s</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Video:</span>
|
||||
<span class="info-value">${stream.video?.codec || "N/A"} ${stream.video?.profile || ""}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Audio:</span>
|
||||
<span class="info-value">${stream.audio?.codec || "N/A"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${watchUrl}" class="stream-watch-link">
|
||||
<div class="stream-url" onclick="navigator.clipboard.writeText('${this.escapeHtml(watchUrl)}').then(() => this.style.background = 'rgba(34, 197, 94, 0.2)').catch(() => {})" title="Click to copy">
|
||||
<span class="stream-url-value">${this.escapeHtml(watchUrl)}</span>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.elements.loadingState.style.display = "block";
|
||||
this.elements.errorState.style.display = "none";
|
||||
this.elements.streamsContainer.style.display = "none";
|
||||
this.elements.noResults.style.display = "none";
|
||||
}
|
||||
|
||||
showError() {
|
||||
this.elements.loadingState.style.display = "none";
|
||||
this.elements.errorState.style.display = "block";
|
||||
this.elements.streamsContainer.style.display = "none";
|
||||
this.elements.noResults.style.display = "none";
|
||||
}
|
||||
|
||||
showStreams() {
|
||||
this.elements.loadingState.style.display = "none";
|
||||
this.elements.errorState.style.display = "none";
|
||||
this.elements.streamsContainer.style.display = "grid";
|
||||
this.elements.noResults.style.display = "none";
|
||||
}
|
||||
|
||||
showNoResults() {
|
||||
this.elements.loadingState.style.display = "none";
|
||||
this.elements.errorState.style.display = "none";
|
||||
this.elements.streamsContainer.style.display = "none";
|
||||
this.elements.noResults.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
new StreamDashboard();
|
Loading…
Add table
Add a link
Reference in a new issue