- Created a new login page for admin access at frontend/admin/login/index.html. - Implemented form submission with error handling for login credentials. - Updated frontend/index.html to mute the video player and removed unnecessary Alpine.js data binding. - Enhanced manifest URL resolution in frontend/src/main.ts to include cache-busting parameters. - Modified vite.config.ts to include the new admin login page in the build process. - Updated nginx.conf to handle admin login routing, authentication, and API proxying for admin actions.
205 lines
5.7 KiB
TypeScript
205 lines
5.7 KiB
TypeScript
import Hls from "hls.js";
|
|
import "./style.css";
|
|
|
|
// Main application state with Alpine.js
|
|
declare global {
|
|
interface Window {
|
|
app: () => any;
|
|
switchRoom: (room: string) => void;
|
|
sendMessage: () => void;
|
|
togglePrivateDM: () => void;
|
|
}
|
|
}
|
|
|
|
let hls: Hls | null = null;
|
|
let ws: WebSocket | null = null;
|
|
let currentRoom = "main";
|
|
let username = "viewer" + Math.floor(Math.random() * 1000);
|
|
|
|
function resolveWebSocketBaseUrl() {
|
|
const configuredUrl = import.meta.env.VITE_PUBLIC_WS_URL?.trim();
|
|
|
|
if (configuredUrl) {
|
|
return configuredUrl;
|
|
}
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
return `${protocol}//${window.location.host}/ws`;
|
|
}
|
|
|
|
function resolveManifestUrl() {
|
|
const configuredUrl = import.meta.env.VITE_PUBLIC_HLS_URL?.trim();
|
|
const cacheBust = `stream=${Date.now()}`;
|
|
const sameOriginManifestUrl = new URL(
|
|
"/hls/playlist.m3u8",
|
|
window.location.origin,
|
|
);
|
|
sameOriginManifestUrl.searchParams.set("stream", cacheBust);
|
|
|
|
const allowCrossOriginManifest =
|
|
import.meta.env.VITE_PUBLIC_HLS_CROSS_ORIGIN?.trim().toLowerCase() ===
|
|
"true";
|
|
|
|
if (configuredUrl) {
|
|
const manifestUrl = new URL(configuredUrl);
|
|
manifestUrl.searchParams.set("stream", cacheBust);
|
|
|
|
if (
|
|
!allowCrossOriginManifest &&
|
|
manifestUrl.origin !== window.location.origin
|
|
) {
|
|
return sameOriginManifestUrl.toString();
|
|
}
|
|
|
|
return manifestUrl.toString();
|
|
}
|
|
|
|
return sameOriginManifestUrl.toString();
|
|
}
|
|
|
|
function connectWebSocket(room: string) {
|
|
if (ws) ws.close();
|
|
|
|
const socketUrl = new URL(resolveWebSocketBaseUrl());
|
|
socketUrl.searchParams.set("username", username);
|
|
socketUrl.searchParams.set("room", room);
|
|
ws = new WebSocket(socketUrl);
|
|
|
|
ws.onopen = () => {
|
|
console.log(`Connected to chat room: ${room}`);
|
|
addMessage("system", "Connected to " + room);
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
console.log("Chat disconnected. Reconnecting in 3s...");
|
|
setTimeout(() => connectWebSocket(room), 3000);
|
|
};
|
|
}
|
|
|
|
function addMessage(
|
|
type: string,
|
|
text: string,
|
|
sender = "",
|
|
timestamp = new Date(),
|
|
) {
|
|
const messages = document.getElementById("messages")!;
|
|
const div = document.createElement("div");
|
|
div.className = `message flex ${type === "system" ? "justify-center" : sender === username ? "justify-end" : "justify-start"}`;
|
|
|
|
if (type === "system") {
|
|
div.innerHTML = `<div class="text-xs text-slate-400 bg-slate-800 px-3 py-1 rounded-full">${text}</div>`;
|
|
} else {
|
|
div.innerHTML = `
|
|
<div class="${sender === username ? "bg-blue-600" : "bg-slate-700"} px-4 py-2 rounded-3xl max-w-[75%]">
|
|
${sender && sender !== username ? `<div class="text-xs text-blue-300">${sender}</div>` : ""}
|
|
<div>${text}</div>
|
|
<div class="text-[10px] text-slate-400 mt-1">${new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
messages.appendChild(div);
|
|
messages.scrollTop = messages.scrollHeight;
|
|
}
|
|
|
|
function sendMessage() {
|
|
const input = document.getElementById("message-input") as HTMLInputElement;
|
|
const text = input.value.trim();
|
|
if (!text || !ws) return;
|
|
|
|
const msg = {
|
|
room: currentRoom,
|
|
username: username,
|
|
text: text,
|
|
type: currentRoom.startsWith("dm:") ? "private" : "public",
|
|
};
|
|
|
|
ws.send(JSON.stringify(msg));
|
|
input.value = "";
|
|
}
|
|
|
|
function switchRoom(room: string) {
|
|
currentRoom = room;
|
|
document.getElementById("room-name")!.textContent =
|
|
room === "main"
|
|
? "Main Channel"
|
|
: room.charAt(0).toUpperCase() + room.slice(1) + " Room";
|
|
addMessage("system", `Switched to ${room} room`);
|
|
connectWebSocket(room);
|
|
}
|
|
|
|
function togglePrivateDM() {
|
|
const target = prompt("Enter username to start private chat with:");
|
|
if (target && target !== username) {
|
|
const dmRoom = `dm:${[username, target].sort().join("-")}`;
|
|
switchRoom(dmRoom);
|
|
}
|
|
}
|
|
|
|
// Alpine.js app
|
|
window.app = () => {
|
|
return {
|
|
init() {
|
|
// Initialize Tailwind
|
|
// @ts-ignore
|
|
tailwind.config = {
|
|
content: [],
|
|
theme: { extend: {} },
|
|
};
|
|
|
|
// Initialize HLS Player
|
|
const video = document.getElementById("player") as HTMLVideoElement;
|
|
const manifestUrl = resolveManifestUrl();
|
|
|
|
if (Hls.isSupported()) {
|
|
hls = new Hls();
|
|
hls.loadSource(manifestUrl);
|
|
hls.attachMedia(video);
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play());
|
|
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
video.src = manifestUrl;
|
|
video.addEventListener("loadedmetadata", () => video.play());
|
|
}
|
|
|
|
// Connect to default chat room
|
|
connectWebSocket("main");
|
|
|
|
// Simulate now playing
|
|
setInterval(() => {
|
|
const titles = [
|
|
"Midnight Jazz Lounge",
|
|
"Synthwave Nights",
|
|
"Classic Cinema",
|
|
"Ambient Void",
|
|
];
|
|
document.getElementById("now-playing")!.textContent =
|
|
titles[Math.floor(Math.random() * titles.length)];
|
|
const overlay = document.getElementById("now-playing-overlay")!;
|
|
overlay.classList.remove("hidden");
|
|
document.getElementById("overlay-title")!.textContent =
|
|
titles[Math.floor(Math.random() * titles.length)];
|
|
}, 15000);
|
|
|
|
console.log(
|
|
"Linear Void Frontend initialized - HLS Player + Chat Sidebar ready",
|
|
);
|
|
},
|
|
};
|
|
};
|
|
|
|
// Make functions global for onclick handlers
|
|
window.switchRoom = switchRoom;
|
|
window.sendMessage = sendMessage;
|
|
window.togglePrivateDM = togglePrivateDM;
|
|
|
|
// Auto initialize when loaded
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// @ts-ignore
|
|
window.app().init();
|
|
});
|