initial commit
This commit is contained in:
commit
44fcd3e5ac
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
config.json
|
||||||
|
todo.md
|
6
config.example.json
Normal file
6
config.example.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"ipAddress": "192.168.1.1",
|
||||||
|
"port": 8080
|
||||||
|
}
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module radvoice
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require github.com/gorilla/websocket v1.5.3
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
466
main.go
Normal file
466
main.go
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
IpAddress string `json:"ipAddress"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
} `json:"server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(filepath string) Config {
|
||||||
|
contents, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading config file: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
err = json.Unmarshal(contents, &config)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing config file: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
username string
|
||||||
|
id string
|
||||||
|
hub *Hub
|
||||||
|
send chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hub struct {
|
||||||
|
clients map[*Client]bool
|
||||||
|
broadcast chan []byte
|
||||||
|
register chan *Client
|
||||||
|
unregister chan *Client
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
UserID string `json:"userId,omitempty"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
Offer any `json:"offer,omitempty"`
|
||||||
|
Answer any `json:"answer,omitempty"`
|
||||||
|
ICE any `json:"ice,omitempty"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHub() *Hub {
|
||||||
|
return &Hub{
|
||||||
|
clients: make(map[*Client]bool),
|
||||||
|
broadcast: make(chan []byte, 256), // Add buffer to prevent blocking
|
||||||
|
register: make(chan *Client, 256),
|
||||||
|
unregister: make(chan *Client, 256),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) run() {
|
||||||
|
log.Println("Hub started and running...")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case client := <-h.register:
|
||||||
|
h.mutex.Lock()
|
||||||
|
h.clients[client] = true
|
||||||
|
h.mutex.Unlock()
|
||||||
|
|
||||||
|
//log.Printf("Client registered: %s (total clients: %d)", client.id, len(h.clients))
|
||||||
|
|
||||||
|
// Send current users list to new client
|
||||||
|
h.sendUsersList()
|
||||||
|
|
||||||
|
case client := <-h.unregister:
|
||||||
|
h.mutex.Lock()
|
||||||
|
if _, ok := h.clients[client]; ok {
|
||||||
|
delete(h.clients, client)
|
||||||
|
close(client.send)
|
||||||
|
//log.Printf("Client unregistered: %s (total clients: %d)", client.id, len(h.clients))
|
||||||
|
}
|
||||||
|
h.mutex.Unlock()
|
||||||
|
|
||||||
|
// Send updated users list
|
||||||
|
h.sendUsersList()
|
||||||
|
|
||||||
|
case message := <-h.broadcast:
|
||||||
|
//log.Printf("Broadcasting message to %d clients: %s", len(h.clients), string(message))
|
||||||
|
h.mutex.RLock()
|
||||||
|
successCount := 0
|
||||||
|
for client := range h.clients {
|
||||||
|
select {
|
||||||
|
case client.send <- message:
|
||||||
|
successCount++
|
||||||
|
//log.Printf("Message queued for client %s", client.id)
|
||||||
|
default:
|
||||||
|
//log.Printf("Failed to queue message for client %s, closing connection", client.id)
|
||||||
|
close(client.send)
|
||||||
|
delete(h.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mutex.RUnlock()
|
||||||
|
//log.Printf("Message broadcast completed. Successful sends: %d", successCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) isUsernameTaken(username string) bool {
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
for client := range h.clients {
|
||||||
|
if client.username == username {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) sendUsersList() {
|
||||||
|
h.mutex.RLock()
|
||||||
|
users := make([]map[string]string, 0)
|
||||||
|
for client := range h.clients {
|
||||||
|
if client.username != "" {
|
||||||
|
users = append(users, map[string]string{
|
||||||
|
"id": client.id,
|
||||||
|
"username": client.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mutex.RUnlock()
|
||||||
|
|
||||||
|
//log.Printf("sendUsersList: Found %d users with usernames", len(users))
|
||||||
|
//log.Printf("sendUsersList: Users data: %+v", users)
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
Type: "users_list",
|
||||||
|
Data: users,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("sendUsersList: Error marshaling message: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("sendUsersList: About to send to broadcast channel: %s", string(data))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case h.broadcast <- data:
|
||||||
|
//log.Printf("sendUsersList: Message sent to broadcast channel successfully")
|
||||||
|
default:
|
||||||
|
//log.Printf("sendUsersList: WARNING - broadcast channel is full or blocked!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) sendToClient(targetID string, message []byte) {
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
for client := range h.clients {
|
||||||
|
if client.id == targetID {
|
||||||
|
select {
|
||||||
|
case client.send <- message:
|
||||||
|
default:
|
||||||
|
close(client.send)
|
||||||
|
delete(h.clients, client)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) readPump() {
|
||||||
|
defer func() {
|
||||||
|
c.hub.unregister <- c
|
||||||
|
c.conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var msg Message
|
||||||
|
err := c.conn.ReadJSON(&msg)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("Error reading message: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case "set_username":
|
||||||
|
if username, ok := msg.Data.(string); ok {
|
||||||
|
//log.Printf("Username request for client %s: %s", c.id, username)
|
||||||
|
|
||||||
|
// Check if username is already taken
|
||||||
|
if c.hub.isUsernameTaken(username) {
|
||||||
|
//log.Printf("Username '%s' is already taken", username)
|
||||||
|
|
||||||
|
// Send error message back to client
|
||||||
|
errorMsg := Message{
|
||||||
|
Type: "username_error",
|
||||||
|
Error: "Username is already taken. Please choose a different username.",
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(errorMsg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case c.send <- data:
|
||||||
|
//log.Printf("Sent username error to client %s", c.id)
|
||||||
|
default:
|
||||||
|
//log.Printf("Failed to send username error to client %s", c.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//log.Printf("Setting username for client %s: %s", c.id, username)
|
||||||
|
c.username = username
|
||||||
|
c.hub.sendUsersList()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//log.Printf("Invalid username data type: %T", msg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "webrtc_offer":
|
||||||
|
// Forward WebRTC offer to target client
|
||||||
|
data, _ := json.Marshal(Message{
|
||||||
|
Type: "webrtc_offer",
|
||||||
|
UserID: c.id,
|
||||||
|
Username: c.username,
|
||||||
|
Offer: msg.Offer,
|
||||||
|
})
|
||||||
|
c.hub.sendToClient(msg.Target, data)
|
||||||
|
|
||||||
|
case "webrtc_answer":
|
||||||
|
// Forward WebRTC answer to target client
|
||||||
|
data, _ := json.Marshal(Message{
|
||||||
|
Type: "webrtc_answer",
|
||||||
|
UserID: c.id,
|
||||||
|
Answer: msg.Answer,
|
||||||
|
})
|
||||||
|
c.hub.sendToClient(msg.Target, data)
|
||||||
|
|
||||||
|
case "webrtc_ice":
|
||||||
|
// Forward ICE candidate to target client
|
||||||
|
data, _ := json.Marshal(Message{
|
||||||
|
Type: "webrtc_ice",
|
||||||
|
UserID: c.id,
|
||||||
|
ICE: msg.ICE,
|
||||||
|
})
|
||||||
|
c.hub.sendToClient(msg.Target, data)
|
||||||
|
|
||||||
|
case "speaking":
|
||||||
|
// Broadcast speaking status
|
||||||
|
broadcastMsg := Message{
|
||||||
|
Type: "user_speaking",
|
||||||
|
UserID: c.id,
|
||||||
|
Username: c.username,
|
||||||
|
Data: msg.Data,
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(broadcastMsg)
|
||||||
|
c.hub.broadcast <- data
|
||||||
|
|
||||||
|
case "chat_message":
|
||||||
|
// Broadcast chat message to all users
|
||||||
|
if msg.Username != "" {
|
||||||
|
// Extract message content from Data field
|
||||||
|
var chatMsg string
|
||||||
|
if msgData, ok := msg.Data.(map[string]any); ok {
|
||||||
|
if message, exists := msgData["message"]; exists {
|
||||||
|
chatMsg, _ = message.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use timestamp from message or current time
|
||||||
|
timestamp := msg.Timestamp
|
||||||
|
if timestamp == 0 {
|
||||||
|
timestamp = time.Now().UnixMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
if chatMsg != "" {
|
||||||
|
//log.Printf("Chat message from %s (%s): %s", msg.Username, c.id, chatMsg)
|
||||||
|
|
||||||
|
broadcastMsg := Message{
|
||||||
|
Type: "chat_message",
|
||||||
|
Username: msg.Username,
|
||||||
|
Data: map[string]any{
|
||||||
|
"message": chatMsg,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(broadcastMsg)
|
||||||
|
c.hub.broadcast <- data
|
||||||
|
} else {
|
||||||
|
//log.Printf("Empty chat message from client %s", c.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//log.Printf("Invalid chat message format from client %s", c.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) writePump() {
|
||||||
|
defer c.conn.Close()
|
||||||
|
|
||||||
|
for message := range c.send {
|
||||||
|
//log.Printf("Sending message to client %s: %s", c.id, string(message))
|
||||||
|
err := c.conn.WriteMessage(websocket.TextMessage, message)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("Error sending message to client %s: %v", c.id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//log.Printf("Message sent successfully to client %s", c.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
//log.Printf("WebSocket upgrade error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a simple ID (in production, use a proper UUID library)
|
||||||
|
clientID := generateID()
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
conn: conn,
|
||||||
|
id: clientID,
|
||||||
|
hub: hub,
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("New client connected")
|
||||||
|
client.hub.register <- client
|
||||||
|
|
||||||
|
// Important: Start both pumps as goroutines
|
||||||
|
go client.writePump()
|
||||||
|
go client.readPump()
|
||||||
|
|
||||||
|
//log.Printf("Started read and write pumps for client: %s", clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateID() string {
|
||||||
|
// Simple ID generation using current time
|
||||||
|
return fmt.Sprintf("user_%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
return g.Writer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GzipMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUsernameCheck(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Check if username is taken
|
||||||
|
isTaken := hub.isUsernameTaken(request.Username)
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"available": !isTaken,
|
||||||
|
"username": request.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
var configPath = flag.String("config", "config.json", "Path to the configuration file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
absPath, err := filepath.Abs(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error getting absolute path:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(absPath)
|
||||||
|
|
||||||
|
*configPath = absPath
|
||||||
|
|
||||||
|
if _, err := os.Stat(*configPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Configuration file not found: %s\n", *configPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := LoadConfig(*configPath)
|
||||||
|
address := fmt.Sprintf("%s:%d", config.Server.IpAddress, config.Server.Port)
|
||||||
|
|
||||||
|
hub := newHub()
|
||||||
|
go hub.run()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleWebSocket(hub, w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/check-username", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleUsernameCheck(hub, w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Serve the index.html file
|
||||||
|
fileServer := http.FileServer(http.Dir("./static/"))
|
||||||
|
GzipMiddleware(fileServer).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Voice chat server starting on %s...\n", address)
|
||||||
|
log.Fatal(http.ListenAndServe(address, mux))
|
||||||
|
}
|
1746
static/app.js
Normal file
1746
static/app.js
Normal file
File diff suppressed because it is too large
Load Diff
173
static/index.html
Normal file
173
static/index.html
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>RadChat</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css">
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Left Sidebar for Users and Voice Controls -->
|
||||||
|
<div class="sidebar" id="sidebar">
|
||||||
|
<!-- Users List -->
|
||||||
|
<div class="sidebar-section users-section">
|
||||||
|
<!-- <div class="sidebar-header"> -->
|
||||||
|
<!-- <h3>👥 Users (<span id="user-count">0</span>)</h3> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<div id="users-list" class="users-list">
|
||||||
|
<div class="no-users"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Voice Controls in Sidebar -->
|
||||||
|
<div class="sidebar-section voice-controls-sidebar" id="voice-controls-sidebar" style="display: none;">
|
||||||
|
<!-- <div class="sidebar-header"> -->
|
||||||
|
<!-- <h3>🎙️ Voice Controls</h3> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<div class="voice-controls-content">
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button
|
||||||
|
id="toggle-mute-btn"
|
||||||
|
onclick="toggleMute()"
|
||||||
|
class="voice-btn unmuted"
|
||||||
|
>
|
||||||
|
🎤
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="toggle-deafen-btn"
|
||||||
|
onclick="toggleDeafen()"
|
||||||
|
class="voice-btn"
|
||||||
|
>
|
||||||
|
🔊
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="voice-settings-btn"
|
||||||
|
onclick="openSelfModal()"
|
||||||
|
class="voice-btn"
|
||||||
|
>
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="leave-voice-btn"
|
||||||
|
onclick="leaveVoiceChat()"
|
||||||
|
class="voice-btn danger"
|
||||||
|
>
|
||||||
|
📞
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- <header> -->
|
||||||
|
<!-- <h1>RadChat</h1> -->
|
||||||
|
<!-- <div class="connection-status" id="status" display="none"> -->
|
||||||
|
<!-- <span class="status-indicator disconnected"></span> -->
|
||||||
|
<!-- <span id="status-text">Disconnected</span> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- </header> -->
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Username Section - Always visible when not in voice chat -->
|
||||||
|
<div class="username-section" id="username-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>RadChat</h2>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username-input"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
maxlength="20"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="join-chat-btn"
|
||||||
|
onclick="joinVoiceChat()"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
📞 Join Voice Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Microphone Permission -->
|
||||||
|
<div class="mic-section" id="mic-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>🎤 Microphone Access</h2>
|
||||||
|
<p>Grant microphone access to participate in voice chat</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="request-mic-btn"
|
||||||
|
onclick="requestMicrophonePermission()"
|
||||||
|
class="mic-button"
|
||||||
|
>
|
||||||
|
🎤 Request Microphone Access
|
||||||
|
</button>
|
||||||
|
<div id="mic-status" class="mic-status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Section -->
|
||||||
|
<div class="chat-section" id="chat-section" style="display: none;">
|
||||||
|
<!-- <div class="section-header"> -->
|
||||||
|
<!-- <h2>💬 Chat</h2> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<div class="chat-messages" id="chat-messages">
|
||||||
|
</div>
|
||||||
|
<div class="chat-input-container">
|
||||||
|
<textarea
|
||||||
|
id="chat-input"
|
||||||
|
placeholder="Type a message... (Shift+Enter for new line)"
|
||||||
|
maxlength="500"
|
||||||
|
rows="1"
|
||||||
|
onkeypress="handleChatKeyPress(event)"
|
||||||
|
oninput="handleChatInput(event)"
|
||||||
|
></textarea>
|
||||||
|
<button id="send-btn" onclick="sendChatMessage()" disabled>Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio Elements Container -->
|
||||||
|
<div id="audio-container" style="display: none;"></div>
|
||||||
|
|
||||||
|
<!-- User Control Modal -->
|
||||||
|
<div id="user-control-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modal-username">User Controls</h3>
|
||||||
|
<button class="modal-close" onclick="closeUserModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="control-section">
|
||||||
|
<label>Volume Control</label>
|
||||||
|
<div class="volume-control">
|
||||||
|
<span>🔇</span>
|
||||||
|
<input type="range" id="user-volume-slider" min="0" max="100" value="100" oninput="updateUserVolume()">
|
||||||
|
<span>🔊</span>
|
||||||
|
<span id="volume-percentage">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-section">
|
||||||
|
<label>Audio Controls</label>
|
||||||
|
<div class="audio-controls">
|
||||||
|
<button id="modal-mute-btn" onclick="toggleUserMuteFromModal()" class="control-btn">
|
||||||
|
🔇 Mute User
|
||||||
|
</button>
|
||||||
|
<button onclick="resetUserVolume()" class="control-btn secondary">
|
||||||
|
🔄 Reset Volume
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1386
static/styles.css
Normal file
1386
static/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user