This commit is contained in:
parent
ae3224c18b
commit
a3b03fdec4
14 changed files with 2200 additions and 37 deletions
480
public/js/chat.js
Normal file
480
public/js/chat.js
Normal file
|
@ -0,0 +1,480 @@
|
|||
class ChatManager {
|
||||
constructor() {
|
||||
this.chatId = this.getChatId();
|
||||
this.messages = [];
|
||||
this.websocket = null;
|
||||
this.isDestroyed = false;
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.hasJoinedChat = false;
|
||||
this.hasLoadedHistory = false;
|
||||
|
||||
this.settings = {
|
||||
showImages: localStorage.getItem("chat-show-images") !== "false",
|
||||
use24hTime: localStorage.getItem("chat-24h-time") !== "false",
|
||||
};
|
||||
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
this.loadStoredUsername();
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
getChatId() {
|
||||
const metaTag = document.querySelector('meta[name="chat-id"]');
|
||||
if (!metaTag) {
|
||||
throw new Error("Chat ID not found in meta tags");
|
||||
}
|
||||
return metaTag.content;
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.elements = {
|
||||
loadingState: document.getElementById("loadingState"),
|
||||
errorState: document.getElementById("errorState"),
|
||||
errorMessage: document.getElementById("errorMessage"),
|
||||
retryBtn: document.getElementById("retryBtn"),
|
||||
messagesContainer: document.getElementById("messagesContainer"),
|
||||
emptyState: document.getElementById("emptyState"),
|
||||
chatForm: document.getElementById("chatForm"),
|
||||
usernameInput: document.getElementById("usernameInput"),
|
||||
messageInput: document.getElementById("messageInput"),
|
||||
sendBtn: document.getElementById("sendBtn"),
|
||||
settingsBtn: document.getElementById("settingsBtn"),
|
||||
settingsDropdown: document.getElementById("settingsDropdown"),
|
||||
imageToggle: document.getElementById("imageToggle"),
|
||||
timeToggle: document.getElementById("timeToggle"),
|
||||
};
|
||||
|
||||
this.elements.imageToggle.checked = this.settings.showImages;
|
||||
this.elements.timeToggle.checked = this.settings.use24hTime;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.elements.chatForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
});
|
||||
|
||||
this.elements.retryBtn.addEventListener("click", () => {
|
||||
this.connectWebSocket();
|
||||
});
|
||||
|
||||
this.elements.usernameInput.addEventListener("input", () => {
|
||||
const username = this.elements.usernameInput.value.trim();
|
||||
if (username) {
|
||||
localStorage.setItem("chat-username", username);
|
||||
}
|
||||
this.validateForm();
|
||||
});
|
||||
|
||||
this.elements.messageInput.addEventListener("input", () => {
|
||||
this.validateForm();
|
||||
});
|
||||
|
||||
this.elements.settingsBtn.addEventListener("click", () => {
|
||||
this.toggleSettingsDropdown();
|
||||
});
|
||||
|
||||
this.elements.imageToggle.addEventListener("change", () => {
|
||||
this.settings.showImages = this.elements.imageToggle.checked;
|
||||
localStorage.setItem("chat-show-images", this.settings.showImages);
|
||||
this.renderMessages();
|
||||
});
|
||||
|
||||
this.elements.timeToggle.addEventListener("change", () => {
|
||||
this.settings.use24hTime = this.elements.timeToggle.checked;
|
||||
localStorage.setItem("chat-24h-time", this.settings.use24hTime);
|
||||
this.renderMessages();
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest(".settings-container")) {
|
||||
this.elements.settingsDropdown.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden && !this.isConnected) {
|
||||
this.connectWebSocket();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.cleanup();
|
||||
});
|
||||
|
||||
this.elements.messageInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadStoredUsername() {
|
||||
const storedUsername = localStorage.getItem("chat-username");
|
||||
if (storedUsername) {
|
||||
this.elements.usernameInput.value = storedUsername;
|
||||
}
|
||||
this.validateForm();
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
const username = this.elements.usernameInput.value.trim();
|
||||
const message = this.elements.messageInput.value.trim();
|
||||
const isValid = username.length > 0 && message.length > 0;
|
||||
|
||||
this.elements.sendBtn.disabled = !isValid;
|
||||
}
|
||||
|
||||
connectWebSocket() {
|
||||
if (this.websocket) {
|
||||
this.websocket.close();
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${window.location.host}`;
|
||||
|
||||
try {
|
||||
this.websocket = new WebSocket(wsUrl);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
console.log("WebSocket connected");
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.hasLoadedHistory = false;
|
||||
|
||||
this.loadChatHistory();
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleWebSocketMessage(message);
|
||||
} catch (error) {
|
||||
console.error("Error parsing WebSocket message:", error);
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
this.isConnected = false;
|
||||
};
|
||||
|
||||
this.websocket.onclose = (event) => {
|
||||
console.log("WebSocket closed:", event.code, event.reason);
|
||||
this.isConnected = false;
|
||||
|
||||
if (
|
||||
!this.isDestroyed &&
|
||||
this.reconnectAttempts < this.maxReconnectAttempts
|
||||
) {
|
||||
this.reconnectAttempts++;
|
||||
console.log(
|
||||
`Attempting to reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||
);
|
||||
setTimeout(
|
||||
() => {
|
||||
if (!this.isDestroyed) {
|
||||
this.connectWebSocket();
|
||||
}
|
||||
},
|
||||
Math.min(1000 * this.reconnectAttempts, 5000),
|
||||
);
|
||||
} else if (!this.isDestroyed) {
|
||||
this.showError("Connection lost. Please refresh the page.");
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to create WebSocket:", error);
|
||||
this.showError("Failed to connect to chat server");
|
||||
}
|
||||
}
|
||||
|
||||
loadChatHistory() {
|
||||
if (
|
||||
this.websocket &&
|
||||
this.websocket.readyState === WebSocket.OPEN &&
|
||||
!this.hasLoadedHistory
|
||||
) {
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
type: "load_history",
|
||||
streamId: this.chatId,
|
||||
}),
|
||||
);
|
||||
this.hasLoadedHistory = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleWebSocketMessage(message) {
|
||||
switch (message.type) {
|
||||
case "connected":
|
||||
console.log("Connected to chat server");
|
||||
this.loadChatHistory();
|
||||
break;
|
||||
|
||||
case "chat_history":
|
||||
this.messages = message.data.messages || [];
|
||||
this.renderMessages();
|
||||
this.showMessages();
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
|
||||
case "chat_message":
|
||||
this.messages.push(message.data);
|
||||
this.renderMessages();
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
|
||||
case "user_joined":
|
||||
this.showSystemMessage(`${message.data.username} joined`);
|
||||
break;
|
||||
|
||||
case "user_left":
|
||||
this.showSystemMessage(`${message.data.username} left`);
|
||||
break;
|
||||
|
||||
case "error":
|
||||
console.error("WebSocket error:", message.error);
|
||||
this.showError(message.error);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Unknown message type:", message.type);
|
||||
}
|
||||
}
|
||||
|
||||
joinChat(username) {
|
||||
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
type: "join_chat",
|
||||
streamId: this.chatId,
|
||||
username: username,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
const username = this.elements.usernameInput.value.trim();
|
||||
const message = this.elements.messageInput.value.trim();
|
||||
|
||||
if (!username || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
||||
this.showError("Not connected to chat server");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasJoinedChat) {
|
||||
this.joinChat(username);
|
||||
this.hasJoinedChat = true;
|
||||
}
|
||||
|
||||
this.elements.sendBtn.disabled = true;
|
||||
this.elements.messageInput.disabled = true;
|
||||
|
||||
try {
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
type: "send_message",
|
||||
message: message,
|
||||
}),
|
||||
);
|
||||
|
||||
this.elements.messageInput.value = "";
|
||||
|
||||
localStorage.setItem("chat-username", username);
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
} finally {
|
||||
this.elements.messageInput.disabled = false;
|
||||
this.validateForm();
|
||||
this.elements.messageInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
toggleSettingsDropdown() {
|
||||
const dropdown = this.elements.settingsDropdown;
|
||||
dropdown.style.display =
|
||||
dropdown.style.display === "block" ? "none" : "block";
|
||||
}
|
||||
|
||||
processMessageContent(text) {
|
||||
if (!this.settings.showImages) {
|
||||
return this.escapeHtml(text);
|
||||
}
|
||||
|
||||
const imageUrlRegex =
|
||||
/https:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg)(?:\?[^\s]*)?/gi;
|
||||
|
||||
const parts = text.split(imageUrlRegex);
|
||||
const imageUrls = text.match(imageUrlRegex) || [];
|
||||
|
||||
let processedContent = "";
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i]) {
|
||||
processedContent += this.escapeHtml(parts[i]);
|
||||
}
|
||||
|
||||
if (imageUrls[i]) {
|
||||
processedContent += this.createImageElement(imageUrls[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return processedContent;
|
||||
}
|
||||
|
||||
createImageElement(url) {
|
||||
const imageId = `img_${Math.random().toString(36).substr(2, 9)}`;
|
||||
return `
|
||||
<div class="message-image-container">
|
||||
<img
|
||||
id="${imageId}"
|
||||
src="${this.escapeHtml(url)}"
|
||||
alt="Shared image"
|
||||
class="message-image"
|
||||
loading="lazy"
|
||||
style="max-width: 300px; max-height: 200px; border-radius: 8px; margin: 8px 0; display: block;"
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderMessages() {
|
||||
this.elements.messagesContainer.innerHTML = "";
|
||||
|
||||
if (this.messages.length === 0) {
|
||||
this.showEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const message of this.messages) {
|
||||
const messageElement = this.createMessageElement(message);
|
||||
this.elements.messagesContainer.appendChild(messageElement);
|
||||
}
|
||||
}
|
||||
|
||||
createMessageElement(message) {
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.className = "message";
|
||||
|
||||
const time = new Date(message.timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: !this.settings.use24hTime,
|
||||
});
|
||||
|
||||
const processedContent = this.processMessageContent(message.message);
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-header">
|
||||
<span class="message-username">${this.escapeHtml(message.username)}</span>
|
||||
<span class="message-time">${time}</span>
|
||||
</div>
|
||||
<div class="message-content">${processedContent}</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
const images = messageDiv.querySelectorAll(".message-image");
|
||||
for (const img of images) {
|
||||
img.addEventListener("error", () => {
|
||||
img.style.display = "none";
|
||||
const errorDiv = document.createElement("div");
|
||||
errorDiv.style.cssText =
|
||||
"padding: 8px; color: #ff6b6b; font-style: italic; background: rgba(255,107,107,0.1); border-radius: 4px; margin: 8px 0;";
|
||||
errorDiv.textContent = "Failed to load image";
|
||||
img.parentNode.appendChild(errorDiv);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return messageDiv;
|
||||
}
|
||||
|
||||
showSystemMessage(text) {
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.className = "message system-message";
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-content">
|
||||
<em>${this.escapeHtml(text)}</em>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.elements.messagesContainer.appendChild(messageDiv);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
const container = this.elements.messagesContainer;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.isDestroyed = true;
|
||||
if (this.websocket) {
|
||||
if (this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
type: "leave_chat",
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.websocket.close();
|
||||
this.websocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.elements.loadingState.style.display = "flex";
|
||||
this.elements.errorState.style.display = "none";
|
||||
this.elements.messagesContainer.style.display = "none";
|
||||
this.elements.emptyState.style.display = "none";
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.elements.loadingState.style.display = "none";
|
||||
this.elements.errorState.style.display = "flex";
|
||||
this.elements.messagesContainer.style.display = "none";
|
||||
this.elements.emptyState.style.display = "none";
|
||||
this.elements.errorMessage.textContent = message;
|
||||
}
|
||||
|
||||
showMessages() {
|
||||
this.elements.loadingState.style.display = "none";
|
||||
this.elements.errorState.style.display = "none";
|
||||
this.elements.messagesContainer.style.display = "flex";
|
||||
this.elements.emptyState.style.display = "none";
|
||||
}
|
||||
|
||||
showEmpty() {
|
||||
this.elements.loadingState.style.display = "none";
|
||||
this.elements.errorState.style.display = "none";
|
||||
this.elements.messagesContainer.style.display = "none";
|
||||
this.elements.emptyState.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
new ChatManager();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize ChatManager:", error);
|
||||
}
|
511
public/js/moveableChat.js
Normal file
511
public/js/moveableChat.js
Normal file
|
@ -0,0 +1,511 @@
|
|||
class MoveableChatFrame {
|
||||
constructor() {
|
||||
try {
|
||||
this.initializeElements();
|
||||
if (!this.frame) {
|
||||
console.warn(
|
||||
"MoveableChatFrame: Required elements not found, skipping initialization",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.isMinimized = false;
|
||||
this.dragOffset = { x: 0, y: 0 };
|
||||
this.resizeHandle = null;
|
||||
this.chatLoaded = false;
|
||||
|
||||
this.initializeEventListeners();
|
||||
this.loadSavedState();
|
||||
} catch (error) {
|
||||
console.error("MoveableChatFrame initialization failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.frame = document.getElementById("moveableChatFrame");
|
||||
this.header = document.getElementById("chatFrameHeader");
|
||||
this.toggleBtn = document.getElementById("toggleChatBtn");
|
||||
this.minimizeBtn = document.getElementById("minimizeChatBtn");
|
||||
this.closeBtn = document.getElementById("closeChatBtn");
|
||||
|
||||
if (!this.frame || !this.header || !this.toggleBtn) {
|
||||
console.warn("MoveableChatFrame: Missing required DOM elements");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
if (
|
||||
!this.toggleBtn ||
|
||||
!this.minimizeBtn ||
|
||||
!this.closeBtn ||
|
||||
!this.header
|
||||
) {
|
||||
console.warn("MoveableChatFrame: Cannot bind events - missing elements");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.toggleBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleChat();
|
||||
});
|
||||
|
||||
this.minimizeBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleMinimize();
|
||||
});
|
||||
|
||||
this.closeBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.closeChat();
|
||||
});
|
||||
|
||||
this.header.addEventListener("mousedown", (e) => {
|
||||
if (e.target.closest(".chat-control-btn")) return;
|
||||
e.preventDefault();
|
||||
this.startDragging(e);
|
||||
});
|
||||
|
||||
const resizeHandles = this.frame.querySelectorAll(".resize-handle");
|
||||
for (const handle of resizeHandles) {
|
||||
handle.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
this.startResizing(e, handle);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
"mousemove",
|
||||
(e) => {
|
||||
if (this.isDragging) {
|
||||
this.drag(e);
|
||||
} else if (this.isResizing) {
|
||||
this.resize(e);
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"mouseup",
|
||||
() => {
|
||||
this.stopDragging();
|
||||
this.stopResizing();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
this.header.addEventListener(
|
||||
"touchstart",
|
||||
(e) => {
|
||||
if (e.target.closest(".chat-control-btn")) return;
|
||||
e.preventDefault();
|
||||
this.startDragging(e.touches[0]);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"touchmove",
|
||||
(e) => {
|
||||
if (this.isDragging) {
|
||||
e.preventDefault();
|
||||
this.drag(e.touches[0]);
|
||||
}
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"touchend",
|
||||
() => {
|
||||
this.stopDragging();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
() => {
|
||||
this.constrainToViewport();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error binding MoveableChatFrame events:", error);
|
||||
}
|
||||
}
|
||||
|
||||
toggleChat() {
|
||||
if (!this.frame) return;
|
||||
|
||||
if (this.frame.style.display === "none") {
|
||||
this.openChat();
|
||||
} else {
|
||||
this.closeChat();
|
||||
}
|
||||
}
|
||||
|
||||
openChat() {
|
||||
if (!this.frame) return;
|
||||
|
||||
this.frame.style.display = "flex";
|
||||
document.body.classList.add("chat-open");
|
||||
|
||||
if (!this.chatLoaded) {
|
||||
this.loadChatContent();
|
||||
}
|
||||
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
loadChatContent() {
|
||||
const streamId = document.querySelector('meta[name="stream-id"]')?.content;
|
||||
if (!streamId) {
|
||||
this.showChatError("Stream ID not found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Loading chat for stream ID:", streamId);
|
||||
this.showLoading();
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.className = "chat-frame-iframe";
|
||||
iframe.src = `/chat/${encodeURIComponent(streamId)}`;
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
background: var(--bg-card);
|
||||
`;
|
||||
|
||||
iframe.onload = () => {
|
||||
this.chatLoaded = true;
|
||||
this.hideLoading();
|
||||
console.log("Chat iframe loaded successfully");
|
||||
};
|
||||
|
||||
iframe.onerror = () => {
|
||||
console.error("Failed to load chat iframe");
|
||||
this.showChatError("Failed to load chat");
|
||||
};
|
||||
|
||||
const loadTimeout = setTimeout(() => {
|
||||
if (!this.chatLoaded) {
|
||||
console.warn("Chat loading timeout");
|
||||
this.showChatError("Chat loading timeout");
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
iframe.addEventListener("load", () => {
|
||||
clearTimeout(loadTimeout);
|
||||
});
|
||||
|
||||
const content = this.frame.querySelector(".chat-frame-content");
|
||||
if (content) {
|
||||
content.innerHTML = "";
|
||||
content.appendChild(iframe);
|
||||
}
|
||||
}
|
||||
|
||||
closeChat() {
|
||||
if (!this.frame) return;
|
||||
|
||||
this.frame.style.display = "none";
|
||||
document.body.classList.remove("chat-open");
|
||||
|
||||
const content = this.frame.querySelector(".chat-frame-content");
|
||||
if (content) {
|
||||
content.innerHTML = "";
|
||||
}
|
||||
|
||||
this.chatLoaded = false;
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
toggleMinimize() {
|
||||
if (!this.frame || !this.minimizeBtn) return;
|
||||
|
||||
this.isMinimized = !this.isMinimized;
|
||||
this.frame.classList.toggle("minimized", this.isMinimized);
|
||||
|
||||
const icon = this.minimizeBtn.querySelector("svg path");
|
||||
if (icon) {
|
||||
if (this.isMinimized) {
|
||||
icon.setAttribute("d", "M18 15l-6-6-6 6");
|
||||
this.minimizeBtn.title = "Maximize";
|
||||
} else {
|
||||
icon.setAttribute("d", "M6 9l6 6 6-6");
|
||||
this.minimizeBtn.title = "Minimize";
|
||||
}
|
||||
}
|
||||
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
if (!this.frame) return;
|
||||
|
||||
const loadingDiv = document.createElement("div");
|
||||
loadingDiv.className = "chat-frame-loading";
|
||||
loadingDiv.innerHTML = `
|
||||
<div class="spinner"></div>
|
||||
<span>Loading chat...</span>
|
||||
`;
|
||||
|
||||
const content = this.frame.querySelector(".chat-frame-content");
|
||||
if (content) {
|
||||
content.innerHTML = "";
|
||||
content.appendChild(loadingDiv);
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading() {}
|
||||
|
||||
showChatError(message) {
|
||||
if (!this.frame) return;
|
||||
|
||||
const errorDiv = document.createElement("div");
|
||||
errorDiv.className = "chat-frame-error";
|
||||
errorDiv.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem; color: var(--accent-red); text-align: center;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: 0.5rem;">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
<span style="font-size: 0.875rem;">${message}</span>
|
||||
<button onclick="window.moveableChatFrame?.loadChatContent()"
|
||||
style="margin-top: 0.5rem; padding: 0.25rem 0.5rem; background: var(--accent-blue); color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem;">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const content = this.frame.querySelector(".chat-frame-content");
|
||||
if (content) {
|
||||
content.innerHTML = "";
|
||||
content.appendChild(errorDiv);
|
||||
}
|
||||
}
|
||||
|
||||
startDragging(event) {
|
||||
if (!this.frame) return;
|
||||
|
||||
this.isDragging = true;
|
||||
const rect = this.frame.getBoundingClientRect();
|
||||
this.dragOffset.x = event.clientX - rect.left;
|
||||
this.dragOffset.y = event.clientY - rect.top;
|
||||
this.frame.style.transition = "none";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
drag(event) {
|
||||
if (!this.isDragging || !this.frame) return;
|
||||
|
||||
const x = event.clientX - this.dragOffset.x;
|
||||
const y = event.clientY - this.dragOffset.y;
|
||||
|
||||
const maxX = window.innerWidth - this.frame.offsetWidth;
|
||||
const maxY = window.innerHeight - this.frame.offsetHeight;
|
||||
|
||||
const constrainedX = Math.max(0, Math.min(x, maxX));
|
||||
const constrainedY = Math.max(0, Math.min(y, maxY));
|
||||
|
||||
this.frame.style.left = `${constrainedX}px`;
|
||||
this.frame.style.top = `${constrainedY}px`;
|
||||
this.frame.style.right = "auto";
|
||||
this.frame.style.bottom = "auto";
|
||||
}
|
||||
|
||||
stopDragging() {
|
||||
if (this.isDragging && this.frame) {
|
||||
this.isDragging = false;
|
||||
this.frame.style.transition = "all 0.3s ease";
|
||||
document.body.style.userSelect = "";
|
||||
this.savePosition();
|
||||
}
|
||||
}
|
||||
|
||||
startResizing(event, handle) {
|
||||
if (this.isMinimized || !this.frame) return;
|
||||
|
||||
this.isResizing = true;
|
||||
this.resizeHandle = handle;
|
||||
this.frame.style.transition = "none";
|
||||
document.body.style.userSelect = "none";
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
resize(event) {
|
||||
if (!this.isResizing || !this.resizeHandle || !this.frame) return;
|
||||
|
||||
const rect = this.frame.getBoundingClientRect();
|
||||
const handleClass = this.resizeHandle.className;
|
||||
|
||||
let newWidth = rect.width;
|
||||
let newHeight = rect.height;
|
||||
let newLeft = rect.left;
|
||||
let newTop = rect.top;
|
||||
|
||||
if (handleClass.includes("resize-handle-e")) {
|
||||
newWidth = event.clientX - rect.left;
|
||||
}
|
||||
if (handleClass.includes("resize-handle-w")) {
|
||||
newWidth = rect.right - event.clientX;
|
||||
newLeft = event.clientX;
|
||||
}
|
||||
if (handleClass.includes("resize-handle-s")) {
|
||||
newHeight = event.clientY - rect.top;
|
||||
}
|
||||
if (handleClass.includes("resize-handle-n")) {
|
||||
newHeight = rect.bottom - event.clientY;
|
||||
newTop = event.clientY;
|
||||
}
|
||||
|
||||
newWidth = Math.max(280, Math.min(newWidth, window.innerWidth));
|
||||
newHeight = Math.max(200, Math.min(newHeight, window.innerHeight));
|
||||
|
||||
this.frame.style.width = `${newWidth}px`;
|
||||
this.frame.style.height = `${newHeight}px`;
|
||||
this.frame.style.left = `${newLeft}px`;
|
||||
this.frame.style.top = `${newTop}px`;
|
||||
this.frame.style.right = "auto";
|
||||
this.frame.style.bottom = "auto";
|
||||
}
|
||||
|
||||
stopResizing() {
|
||||
if (this.isResizing && this.frame) {
|
||||
this.isResizing = false;
|
||||
this.resizeHandle = null;
|
||||
this.frame.style.transition = "all 0.3s ease";
|
||||
document.body.style.userSelect = "";
|
||||
this.savePosition();
|
||||
}
|
||||
}
|
||||
|
||||
constrainToViewport() {
|
||||
if (!this.frame || this.frame.style.display === "none") return;
|
||||
|
||||
const rect = this.frame.getBoundingClientRect();
|
||||
const maxX = window.innerWidth - rect.width;
|
||||
const maxY = window.innerHeight - rect.height;
|
||||
|
||||
const currentX = Number.parseInt(this.frame.style.left) || rect.left;
|
||||
const currentY = Number.parseInt(this.frame.style.top) || rect.top;
|
||||
|
||||
const constrainedX = Math.max(0, Math.min(currentX, maxX));
|
||||
const constrainedY = Math.max(0, Math.min(currentY, maxY));
|
||||
|
||||
this.frame.style.left = `${constrainedX}px`;
|
||||
this.frame.style.top = `${constrainedY}px`;
|
||||
this.frame.style.right = "auto";
|
||||
this.frame.style.bottom = "auto";
|
||||
}
|
||||
|
||||
savePosition() {
|
||||
if (!this.frame) return;
|
||||
|
||||
try {
|
||||
const rect = this.frame.getBoundingClientRect();
|
||||
const position = {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
localStorage.setItem("moveable-chat-position", JSON.stringify(position));
|
||||
} catch (error) {
|
||||
console.warn("Failed to save chat position:", error);
|
||||
}
|
||||
}
|
||||
|
||||
saveState() {
|
||||
if (!this.frame) return;
|
||||
|
||||
try {
|
||||
const state = {
|
||||
visible: this.frame.style.display !== "none",
|
||||
minimized: this.isMinimized,
|
||||
};
|
||||
localStorage.setItem("moveable-chat-state", JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.warn("Failed to save chat state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedState() {
|
||||
if (!this.frame) return;
|
||||
|
||||
const savedPosition = localStorage.getItem("moveable-chat-position");
|
||||
if (savedPosition) {
|
||||
try {
|
||||
const position = JSON.parse(savedPosition);
|
||||
this.frame.style.left = `${position.left}px`;
|
||||
this.frame.style.top = `${position.top}px`;
|
||||
this.frame.style.width = `${position.width}px`;
|
||||
this.frame.style.height = `${position.height}px`;
|
||||
this.frame.style.right = "auto";
|
||||
this.frame.style.bottom = "auto";
|
||||
} catch (e) {
|
||||
console.warn("Failed to load saved chat position:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const savedState = localStorage.getItem("moveable-chat-state");
|
||||
if (savedState) {
|
||||
try {
|
||||
const state = JSON.parse(savedState);
|
||||
if (state.visible) {
|
||||
this.openChat();
|
||||
}
|
||||
if (state.minimized) {
|
||||
this.isMinimized = true;
|
||||
this.toggleMinimize();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load saved chat state:", e);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.constrainToViewport();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeMoveableChat() {
|
||||
try {
|
||||
const requiredElements = [
|
||||
"moveableChatFrame",
|
||||
"chatFrameHeader",
|
||||
"toggleChatBtn",
|
||||
];
|
||||
|
||||
const missingElements = requiredElements.filter(
|
||||
(id) => !document.getElementById(id),
|
||||
);
|
||||
|
||||
if (missingElements.length > 0) {
|
||||
console.warn(
|
||||
"MoveableChatFrame: Missing required elements:",
|
||||
missingElements,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window.moveableChatFrame = new MoveableChatFrame();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize MoveableChatFrame:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(initializeMoveableChat, 100);
|
|
@ -49,12 +49,9 @@ class StreamWatcher {
|
|||
bitrate: document.getElementById("bitrate"),
|
||||
quality: document.getElementById("quality"),
|
||||
streamId: document.getElementById("streamId"),
|
||||
streamApp: document.getElementById("streamApp"),
|
||||
streamDuration: document.getElementById("streamDuration"),
|
||||
videoCodec: document.getElementById("videoCodec"),
|
||||
audioCodec: document.getElementById("audioCodec"),
|
||||
resolution: document.getElementById("resolution"),
|
||||
rtmpUrl: document.getElementById("rtmpUrl"),
|
||||
hlsUrl: document.getElementById("hlsUrl"),
|
||||
flvUrl: document.getElementById("flvUrl"),
|
||||
webrtcUrl: document.getElementById("webrtcUrl"),
|
||||
|
@ -103,7 +100,6 @@ class StreamWatcher {
|
|||
});
|
||||
|
||||
const urlElements = [
|
||||
this.elements.rtmpUrl,
|
||||
this.elements.hlsUrl,
|
||||
this.elements.flvUrl,
|
||||
this.elements.webrtcUrl,
|
||||
|
@ -228,10 +224,6 @@ class StreamWatcher {
|
|||
}
|
||||
|
||||
this.elements.streamId.textContent = stream.name || this.streamId;
|
||||
this.elements.streamApp.textContent = stream.app || "-";
|
||||
this.elements.streamDuration.textContent = isOnline
|
||||
? this.formatDuration(stream.publish?.duration)
|
||||
: "-";
|
||||
this.elements.videoCodec.textContent = stream.video
|
||||
? `${stream.video.codec} ${stream.video.profile || ""}`.trim()
|
||||
: "-";
|
||||
|
@ -247,7 +239,6 @@ class StreamWatcher {
|
|||
const srsHost = new URL(this.srsUrl).host;
|
||||
const streamPath = `${stream.app}/${stream.name}`;
|
||||
|
||||
this.elements.rtmpUrl.textContent = `rtmp://${srsHost}/${streamPath}`;
|
||||
this.elements.hlsUrl.textContent = `${this.srsUrl}/${streamPath}.m3u8`;
|
||||
this.elements.flvUrl.textContent = `${this.srsUrl}/${streamPath}.flv`;
|
||||
this.elements.webrtcUrl.textContent = `webrtc://${srsHost}/${streamPath}`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue