Commit ee7ebecd authored by nextime's avatar nextime

Client base web interface ready

parent 388b43de
......@@ -13,10 +13,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Flask, render_template, request, session
from flask import Flask, render_template, request, session, jsonify
from utils import check_port_available, run_command, create_daemon, run_action
import sys
import os
import json
import time
import threading
from datetime import datetime
from guiutils import get_buttons
import flask_restful as restful
......@@ -97,8 +101,7 @@ class AppData(restful.Resource):
return {'content': content}
@flask_app.route('/')
def index():
def prepare_panel_page():
#buttons, numrows = get_buttons()
row = 1
style_rows=""
......@@ -107,7 +110,7 @@ def index():
while row <= numrows:
bspan = int(120/len(buttons[row].keys()))
style_rows=style_rows+"\n.button_row"+str(row)+" {\n grid-column: span "+str(bspan)+";\n}\n"
col=0
htmlbuttons=htmlbuttons+"<div class='button_row'>"
for b in buttons[row].keys():
......@@ -123,14 +126,23 @@ def index():
htmlbuttons=htmlbuttons+"""<button style="color:white;background-color:"""+color+""";"
class="button private button_row"""+str(row)+" "+pollclass+""" " onclick="executeCommand('"""+command+"""')">
"""+buttons[row][b]['title']+"""
</button>"""
</button>"""
htmlbuttons=htmlbuttons+"</div>"
row=row+1
return style_rows, htmlbuttons
@flask_app.route('/')
def index():
style_rows, htmlbuttons = prepare_panel_page()
return render_template('index.html', style_rows=style_rows, htmlbuttons=htmlbuttons)
@flask_app.route('/panel')
def panel():
style_rows, htmlbuttons = prepare_panel_page()
return render_template('panel.html', style_rows=style_rows, htmlbuttons=htmlbuttons)
@flask_app.route('/execute', methods=['POST'])
def execute():
command_key = request.form.get('command')
......@@ -147,21 +159,392 @@ def stream():
return render_template('stream.html', stream_url=stream_url)
# Fake data for the chat interface
fake_messages = [
{"sender": "me", "content": "Hello! 😊", "timestamp": "2025-06-21 20:00"},
{"sender": "alice@C4.sexhackme", "content": "Hi! Check this: https://example.com", "timestamp": "2025-06-21 20:01"},
{"sender": "bob@SC.spora", "content": "<img src='https://pbs.twimg.com/media/FsypEu2X0AEtwK3?format=jpg&name=small' alt='Placeholder Image'>", "timestamp": "2025-06-21 20:02"},
{"sender": "system", "content": "system: *** Server maintenance scheduled at 1 AM", "type": "notify-system", "timestamp": "2025-06-21 20:03"},
{"sender": "platform@SC.spora", "content": "platform: *** User joined the room", "type": "notify-platform", "timestamp": "2025-06-21 20:04"},
{"sender": "bob@SC.spora", "content": "<span class='sender' data-sender='bob@SC.spora' style='color: #10b981'>bob@SC.spora</span> TIPPED <b>50 TOKENS</b> ($5.00)<div style='text-align: center; color: #6ee7b7'>PM REQUEST</div>", "type": "tip", "timestamp": "2025-06-21 20:05"}
]
fake_users = {
"C4.sexhackme": [
{"username": "alice", "status": "online", "tokens": 50},
{"username": "charlie", "status": "offline", "tokens": 20}
],
"SC.spora": [
{"username": "bob", "status": "online", "tokens": 30}
]
}
fake_earnings = [
{"platform": "C4.sexhackme", "lastSession": 100, "today": 250, "lastHour": 50, "sess": 100},
{"platform": "SC.spora", "lastSession": 80, "today": 200, "lastHour": 30, "sess": 80}
]
fake_status = {
"C4.sexhackme": "online",
"SC.spora": "offline"
}
fake_rtsp_urls = [
{"id": "rtsp1", "url": "rtsp://example.com/stream1"},
{"id": "rtsp2", "url": "rtsp://example.com/stream2"}
]
# Test private chat data
test_private_chat = {
"alice@C4.sexhackme": [
{
"sender": "me",
"content": "Hello Alice!",
"timestamp": datetime.now().isoformat()
},
{
"sender": "alice@C4.sexhackme",
"content": "Hi there! How can I help you?",
"timestamp": datetime.now().isoformat()
}
]
}
# Track unread messages in private chats
unread_private_messages = {
"alice@C4.sexhackme": 1 # Number of unread messages
}
# Store the last update timestamp for each data type
last_updates = {
"messages": time.time(),
"users": time.time(),
"earnings": time.time(),
"status": time.time(),
"rtsp_urls": time.time(),
"private_chats": time.time()
}
# Store the last request timestamp for each client
client_last_request = {}
@flask_app.route("/chat")
def chat():
return render_template('chat.html')
@flask_app.route("/api/chat")
def get_chat_data():
"""
Consolidated API endpoint for chat data with non-blocking long polling.
This implements a hybrid approach:
1. SocketIO for real-time push notifications when data changes
2. Long polling as a fallback mechanism with non-blocking behavior
The client will receive immediate updates via SocketIO when available,
and will fall back to polling if SocketIO is not working.
"""
client_id = request.args.get('client_id', str(time.time()))
# Initialize client's last request time if it's a new client
if client_id not in client_last_request:
# New client - set initial timestamp to 0 to ensure they get data
client_last_request[client_id] = 0
# Get the stored last request time for this client
client_last_time = client_last_request[client_id]
# Check if any data has been updated since the client's last request
def is_data_updated():
# For new clients (last_time=0), always return true
if client_last_time == 0:
return True
# For existing clients, check if any data has been updated
return (last_updates["messages"] > client_last_time or
last_updates["users"] > client_last_time or
last_updates["earnings"] > client_last_time or
last_updates["status"] > client_last_time or
last_updates["rtsp_urls"] > client_last_time or
last_updates["private_chats"] > client_last_time)
# If data is already updated or this is a new client, return immediately
if is_data_updated():
current_time = time.time()
# Update the client's last request time
client_last_request[client_id] = current_time
# Log the data update for debugging
logging.info(f"Sending updated data to client {client_id}")
return jsonify({
"messages": fake_messages,
"users": fake_users,
"earnings": fake_earnings,
"status": fake_status,
"rtsp_urls": fake_rtsp_urls,
"private_chats": test_private_chat,
"unread_private_messages": unread_private_messages,
"timestamp": current_time
})
# For long polling, return a response that will be processed by the client
# This avoids blocking the server thread
return jsonify({
"no_update": True,
"retry_after": 1000, # Retry after 1 second
"timestamp": time.time()
})
# Keep these routes for backward compatibility if needed
@flask_app.route("/api/messages")
def get_messages():
last_updates["messages"] = time.time()
socketio.emit('chat_update', {'type': 'messages'})
return jsonify(fake_messages)
@flask_app.route("/api/users")
def get_users():
last_updates["users"] = time.time()
socketio.emit('chat_update', {'type': 'users'})
return jsonify(fake_users)
@flask_app.route("/api/earnings")
def get_earnings():
last_updates["earnings"] = time.time()
socketio.emit('chat_update', {'type': 'earnings'})
return jsonify(fake_earnings)
@flask_app.route("/api/status")
def get_status():
last_updates["status"] = time.time()
socketio.emit('chat_update', {'type': 'status'})
return jsonify(fake_status)
@flask_app.route("/api/rtsp_urls")
def get_rtsp_urls():
last_updates["rtsp_urls"] = time.time()
socketio.emit('chat_update', {'type': 'rtsp_urls'})
return jsonify(fake_rtsp_urls)
@flask_app.route("/api/private_chats")
def get_private_chats():
last_updates["private_chats"] = time.time()
socketio.emit('chat_update', {'type': 'private_chats'})
return jsonify(test_private_chat)
@flask_app.route("/api/send_message", methods=["POST"])
def send_message():
data = request.json
if not data or "content" not in data:
return jsonify({"error": "Invalid message data"}), 400
# Get client_id from request if available
client_id = request.args.get('client_id', None)
new_message = {
"sender": "me",
"content": data["content"],
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M")
}
# Add to fake messages
fake_messages.append(new_message)
# Update the last update timestamp for messages
current_time = time.time()
last_updates["messages"] = current_time
# Reset all client timestamps to force data refresh on next poll
# But set them to slightly before current time to avoid full refresh
for cid in client_last_request:
# Don't reset the current client's timestamp if we know who it is
if client_id and cid == client_id:
continue
# Set to a value that will trigger an update but not to 0
client_last_request[cid] = current_time - 1
# Notify clients about the update
socketio.emit('chat_update', {'type': 'messages'})
return jsonify({"success": True, "message": new_message})
@flask_app.route("/api/send_private_message", methods=["POST"])
def send_private_message():
data = request.json
if not data or "content" not in data or "recipient" not in data:
return jsonify({"error": "Invalid message data"}), 400
# Get client_id from request if available
client_id = request.args.get('client_id', None)
recipient = data["recipient"]
content = data["content"]
# Create new message
new_message = {
"sender": "me",
"content": content,
"timestamp": datetime.now().isoformat()
}
# Add to private chat
if recipient not in test_private_chat:
test_private_chat[recipient] = []
test_private_chat[recipient].append(new_message)
# Simulate response
response_message = {
"sender": recipient,
"content": f"Thanks for your message: \"{content}\"",
"timestamp": datetime.now().isoformat()
}
test_private_chat[recipient].append(response_message)
# Increment unread message count for the recipient's response
if recipient not in unread_private_messages:
unread_private_messages[recipient] = 0
unread_private_messages[recipient] += 1
# Update the last update timestamp for private chats
current_time = time.time()
last_updates["private_chats"] = current_time
# Reset all client timestamps to force data refresh on next poll
for cid in client_last_request:
# Don't reset the current client's timestamp if we know who it is
if client_id and cid == client_id:
continue
# Set to a value that will trigger an update but not to 0
client_last_request[cid] = current_time - 1
# Notify clients about the update with additional data
socketio.emit('chat_update', {
'type': 'private_chats',
'sender': recipient,
'has_unread': True
})
return jsonify({
"success": True,
"messages": test_private_chat[recipient],
"unread_count": unread_private_messages[recipient]
})
@flask_app.route("/api/mark_messages_read", methods=["POST"])
def mark_messages_read():
data = request.json
if not data or "sender" not in data:
return jsonify({"error": "Invalid data"}), 400
# Get client_id from request if available
client_id = request.args.get('client_id', None)
sender = data["sender"]
# Reset unread count for this sender
if sender in unread_private_messages:
unread_private_messages[sender] = 0
# Update the last update timestamp for private chats
current_time = time.time()
last_updates["private_chats"] = current_time
# Reset all client timestamps to force data refresh on next poll
for cid in client_last_request:
# Don't reset the current client's timestamp if we know who it is
if client_id and cid == client_id:
continue
# Set to a value that will trigger an update but not to 0
client_last_request[cid] = current_time - 1
# Notify clients about the update with specific information
socketio.emit('chat_update', {
'type': 'private_chats',
'sender': sender,
'has_unread': False
})
return jsonify({
"success": True,
"sender": sender,
"unread_count": 0
})
@flask_app.route("/api/reset_session", methods=["POST"])
def reset_session():
# Reset session earnings
for earning in fake_earnings:
earning["sess"] = 0
# Update the last update timestamp for earnings
last_updates["earnings"] = time.time()
# Notify clients about the update
socketio.emit('chat_update', {'type': 'earnings'})
return jsonify({"success": True})
@flask_app.route("/api/update_status", methods=["POST"])
def update_status():
data = request.json
if not data:
return jsonify({"error": "Invalid status data"}), 400
# Update all platforms
if "all" in data and data["all"] in ["online", "offline"]:
new_status = data["all"]
for platform in fake_status:
fake_status[platform] = new_status
# Update specific platform
elif "platform" in data and "status" in data:
platform = data["platform"]
new_status = data["status"]
if platform in fake_status:
fake_status[platform] = new_status
# Update the last update timestamp for status
last_updates["status"] = time.time()
# Notify clients about the update
socketio.emit('chat_update', {'type': 'status'})
return jsonify({"success": True, "status": fake_status})
@socketio.event
def my_event(message):
session['receive_count'] = session.get('receive_count', 0) + 1
emit('my_response',
{'data': message['data'], 'count': session['receive_count']})
@socketio.event
def my_ping():
emit('my_pong')
@socketio.event
def connect():
"""Handle client connection"""
client_id = request.sid
logging.info(f"Client connected: {client_id}")
# Initialize new client with timestamp 0 to ensure they get all data
client_last_request[client_id] = 0
# Emit an immediate update event to trigger data fetch
emit('chat_update', {'type': 'initial_connect'})
@socketio.event
def disconnect():
"""Handle client disconnection"""
client_id = request.sid
logging.info(f"Client disconnected: {client_id}")
if client_id in client_last_request:
del client_last_request[client_id]
@socketio.event
def get_queue():
......@@ -179,7 +562,7 @@ def get_queue():
logging.info('CHANGE THE COLOR OF THE WEB BUTTON FOR '+str(data['output']))
bcfg = outputs[data['output']]['cfg']
if 'color.'+str(data['status']) in bcfg.keys():
emit('change_output', {'button': data['output'], 'color': bcfg['color.'+str(data['status'])] }, broadcast=True)
emit('change_output', {'button': data['output'], 'color': bcfg['color.'+str(data['status'])] }, broadcast=True)
if 'title.'+str(data['status']) in bcfg.keys():
emit('change_output', {'button': data['output'], 'title': bcfg['title.'+str(data['status'])] }, broadcast=True)
if event == 'INPUTSTATUSCHANGE':
......@@ -202,6 +585,27 @@ def get_queue():
# Function to clean up stale client entries
def cleanup_stale_clients():
"""Remove clients that haven't made a request in the last 10 minutes"""
while True:
try:
current_time = time.time()
stale_threshold = current_time - 600 # 10 minutes
# Create a copy of the keys to avoid modifying during iteration
client_ids = list(client_last_request.keys())
for client_id in client_ids:
if client_last_request[client_id] < stale_threshold:
logging.info(f"Removing stale client: {client_id}")
del client_last_request[client_id]
except Exception as e:
logging.error(f"Error in cleanup thread: {e}")
# Sleep for 5 minutes before next cleanup
time.sleep(300)
def run_flask_app(port=5000, daemon_mode=False):
"""Run Flask app with optional daemon mode"""
if not check_port_available(port):
......@@ -213,6 +617,10 @@ def run_flask_app(port=5000, daemon_mode=False):
if daemon_mode and sys.platform != 'win32':
create_daemon()
# Start the cleanup thread
cleanup_thread = threading.Thread(target=cleanup_stale_clients, daemon=True)
cleanup_thread.start()
flask_api.add_resource(PollAPI, '/update')
flask_api.add_resource(AppData, '/data')
#flask_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
......
......@@ -48,6 +48,29 @@
.top-bar a:hover {
text-decoration: underline;
}
/* Auto-open toggle in top bar */
.auto-open-toggle {
display: flex;
align-items: center;
margin-right: 15px;
background: rgba(0, 0, 0, 0.2);
padding: 5px 10px;
border-radius: 20px;
}
.auto-open-toggle label {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: #fff;
}
.auto-open-toggle input[type="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
/* Desktop layout: 3 columns */
.container {
......@@ -101,6 +124,17 @@
/* Video window */
.video-container {
position: relative;
.tab[data-tab="session"] {
background: #2a2a2a;
border-radius: 8px 8px 0 0;
margin-right: 5px;
font-size: 14px;
white-space: nowrap;
}
.tab[data-tab="session"].active {
background: #3b82f6;
}
background: #000;
border-radius: 8px;
overflow: hidden;
......@@ -152,17 +186,22 @@
}
/* Online/offline button */
.status-button-container {
position: relative;
display: flex;
align-items: center;
margin: 10px 0;
flex-shrink: 0;
}
.status-button {
width: 100%;
flex: 1;
padding: 12px;
margin: 10px 0;
font-size: 18px;
font-weight: 600;
border: none;
border-radius: 20px;
border-radius: 20px 0 0 20px;
cursor: pointer;
text-align: center;
flex-shrink: 0;
}
.status-button.online {
background: #dc3545;
......@@ -170,28 +209,67 @@
.status-button.offline {
background: #28a745;
}
.status-toggle {
width: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
border: none;
border-radius: 0 20px 20px 0;
cursor: pointer;
font-size: 16px;
color: #fff;
}
.status-toggle:hover {
background: rgba(0, 0, 0, 0.3);
}
.status-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
width: 300px;
background: #2a2a2a;
border: 1px solid #3b82f6;
padding: 10px;
z-index: 100;
border-radius: 8px;
margin-top: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.status-menu button {
display: block;
display: flex;
justify-content: space-between;
width: 100%;
padding: 8px;
background: none;
background: #135f2a8a;
border: none;
color: #fff;
text-align: left;
cursor: pointer;
border-radius: 20px;
margin-bottom: 5px;
}
.status-menu button:hover {
background: #3b82f6;
.status-menu button[data-status="online"] {
background: #794c4c;
}
.status-menu button:last-child {
margin-bottom: 0;
}
.status-menu button .platform-status {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-left: 8px;
}
.status-menu button .platform-status.online {
background: #28a745;
}
.status-menu button .platform-status.offline {
background: #dc3545;
}
/* Tabs */
......@@ -492,6 +570,23 @@
background: #b91c1c;
}
/* Notification indicator */
.notification-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ff3860;
display: inline-block;
margin-right: 8px;
animation: blink 1s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.3; }
100% { opacity: 1; }
}
/* Private chats list styles */
.private-chats-list {
margin-top: 10px;
......@@ -555,7 +650,11 @@
resize: both;
overflow: hidden;
min-width: 300px;
min-height: 310px;
min-height: 310px;
}
.private-chat-window-container.active {
z-index: 991;
}
.private-chat-content {
......@@ -579,6 +678,17 @@
cursor: move; /* Indicate draggable */
}
.private-chat-title-container {
display: flex;
align-items: center;
gap: 8px;
}
.private-chat-header .notification-indicator {
display: none; /* Hidden by default */
margin-right: 8px;
}
.private-chat-title {
font-size: 16px;
font-weight: 600;
......@@ -708,6 +818,13 @@
<div class="top-bar">
<img src="https://www.sexhack.me/content/uploads/2022/06/cropped-sexhack-300x99.png" alt="SexHack Logo">
<span>SHM CamStudio by <a href="https://www.sexhack.me">sexhack.me</a></span>
<div style="flex: 1;"></div>
<div class="auto-open-toggle">
<label for="auto-open-chats" title="Auto-open private chat windows">
<input type="checkbox" id="auto-open-chats" checked>
<span>Auto-open chats</span>
</label>
</div>
</div>
<div class="container">
<div class="left-column">
......@@ -722,17 +839,33 @@
Bitrate: 2 Mbps | Quality: HD | Audio: <span id="audio-bar">████</span>
</div>
</div>
<button class="status-button offline" id="status-button">GO ONLINE</button>
<div class="status-menu" id="status-menu">
<button data-platform="C4.sexhackme">C4.sexhackme: Offline</button>
<button data-platform="SC.spora">SC.spora: Offline</button>
<div class="status-button-container">
<button class="status-button offline" id="status-button">GO ONLINE</button>
<button class="status-toggle" id="status-toggle"></button>
<div class="status-menu" id="status-menu">
<button data-platform="C4.sexhackme">
C4.sexhackme
<span class="platform-status offline"></span>
</button>
<button data-platform="SC.spora">
SC.spora
<span class="platform-status offline"></span>
</button>
</div>
</div>
<div class="tabs">
<div class="tab active" data-tab="panel">Panel</div>
<div class="tab" data-tab="earnings">Earnings</div>
<div class="tab" data-tab="session">Sessions</div>
</div>
<div class="tab-content active" id="panel">
<iframe id="panel-iframe" style="width:100%;height:300px;border:none;border-radius:8px;"></iframe>
<div class="panel-container" style="position: relative;">
<div class="panel-header" style="display: flex; justify-content: space-between; align-items: center; background: #1e293b; padding: 5px 10px; border-radius: 8px 8px 0 0; margin-bottom: 2px;">
<span style="font-size: 14px; font-weight: 500;">Control Panel</span>
<button id="panel-expand-toggle" style="background: #3b82f6; color: white; border: none; border-radius: 4px; width: 30px; height: 24px; cursor: pointer; font-size: 14px;"></button>
</div>
<iframe id="panel-iframe" style="width:100%;height:300px;border:none;border-radius:0 0 8px 8px;" src="/panel"></iframe>
</div>
</div>
<div class="tab-content" id="earnings">
<table class="earnings-table">
......@@ -774,7 +907,7 @@
<div class="right-column">
<div class="tabs">
<div class="tab active" data-tab="userlist">Userlist (<span id="total-users">0</span>)</div>
<div class="tab" data-tab="private-chats">Private Chats (<span id="private-chats-count">0</span>)</div>
<div class="tab" data-tab="private-chats"><span class="notification-indicator" id="private-chats-tab-indicator" style="display: none;"></span>Private Chats (<span id="private-chats-count">0</span>)</div>
</div>
<div class="tab-content active" id="userlist">
<div class="user-list" id="user-list"></div>
......@@ -797,37 +930,20 @@
<!-- Container for multiple private chat windows -->
<div id="private-chat-windows-container"></div>
<!-- Include Socket.IO client library -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
// Fake data for messages, users, earnings, status, and RTSP URLs
const fakeMessages = [
{ sender: "me", content: "Hello! 😊", timestamp: "2025-06-21 20:00" },
{ sender: "alice@C4.sexhackme", content: "Hi! Check this: https://example.com", timestamp: "2025-06-21 20:01" },
{ sender: "bob@SC.spora", content: "<img src='https://pbs.twimg.com/media/FsypEu2X0AEtwK3?format=jpg&name=small' alt='Placeholder Image'>", timestamp: "2025-06-21 20:02" },
{ sender: "system", content: "system: *** Server maintenance scheduled at 1 AM", type: "notify-system", timestamp: "2025-06-21 20:03" },
{ sender: "platform@SC.spora", content: "platform: *** User joined the room", type: "notify-platform", timestamp: "2025-06-21 20:04" },
{ sender: "bob@SC.spora", content: "<span class='sender' data-sender='bob@SC.spora' style='color: #10b981'>bob@SC.spora</span> TIPPED <b>50 TOKENS</b> ($5.00)<div style='text-align: center; color: #6ee7b7'>PM REQUEST</div>", type: "tip", timestamp: "2025-06-21 20:05" }
];
const fakeUsers = {
"C4.sexhackme": [
{ username: "alice", status: "online", tokens: 50 },
{ username: "charlie", status: "offline", tokens: 20 }
],
"SC.spora": [
{ username: "bob", status: "online", tokens: 30 }
]
};
const fakeEarnings = [
{ platform: "C4.sexhackme", lastSession: 100, today: 250, lastHour: 50, sess: 100 },
{ platform: "SC.spora", lastSession: 80, today: 200, lastHour: 30, sess: 80 }
];
const fakeStatus = {
"C4.sexhackme": "online",
"SC.spora": "offline"
};
const fakeRtspUrls = [
{ id: "rtsp1", url: "rtsp://example.com/stream1" },
{ id: "rtsp2", url: "rtsp://example.com/stream2" }
];
// Data variables that will be populated from the server
let messages = [];
let users = {};
let earnings = [];
let statusData = {};
let rtspUrls = [];
let privateChats = {};
// Configuration variables
let autoOpenPrivateChats = true; // Default to true (enabled)
// Initialize video
const video = document.getElementById('video');
......@@ -848,7 +964,7 @@
} catch (err) {
console.error('Error enumerating devices:', err);
}
fakeRtspUrls.forEach(url => {
rtspUrls.forEach(url => {
const option = document.createElement('option');
option.value = url.id;
option.text = url.url;
......@@ -866,9 +982,9 @@
if (videoSource.value === 'none') {
video.srcObject = null;
video.src = '';
} else if (fakeRtspUrls.find(url => url.id === videoSource.value)) {
} else if (rtspUrls.find(url => url.id === videoSource.value)) {
video.srcObject = null;
video.src = fakeRtspUrls.find(url => url.id === videoSource.value).url;
video.src = rtspUrls.find(url => url.id === videoSource.value).url;
} else {
navigator.mediaDevices.getUserMedia({ video: { deviceId: videoSource.value } })
.then(stream => video.srcObject = stream)
......@@ -906,34 +1022,107 @@
// Status button and menu
const statusButton = document.getElementById('status-button');
const statusToggle = document.getElementById('status-toggle');
const statusMenu = document.getElementById('status-menu');
statusButton.addEventListener('click', (e) => {
// Main status button toggles all platforms at once
statusButton.addEventListener('click', () => {
const isCurrentlyOffline = statusButton.textContent === 'GO ONLINE';
const newStatus = isCurrentlyOffline ? 'online' : 'offline';
// Send status update to server
fetch('/api/update_status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
all: newStatus
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusData = data.status;
updateStatus();
}
})
.catch(error => {
console.error('Error updating status:', error);
});
});
// Toggle button shows/hides the platform menu
statusToggle.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent triggering the main button
statusMenu.style.display = statusMenu.style.display === 'block' ? 'none' : 'block';
statusMenu.style.left = `${e.pageX}px`;
statusMenu.style.top = `${e.pageY}px`;
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!statusToggle.contains(e.target) && !statusMenu.contains(e.target)) {
statusMenu.style.display = 'none';
}
});
// Individual platform buttons
statusMenu.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const platform = btn.dataset.platform;
fakeStatus[platform] = fakeStatus[platform] === 'online' ? 'offline' : 'online';
updateStatus();
statusMenu.style.display = 'none';
const newStatus = statusData[platform] === 'online' ? 'offline' : 'online';
// Send status update to server
fetch('/api/update_status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: platform,
status: newStatus
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusData = data.status;
updateStatus();
}
})
.catch(error => {
console.error('Error updating status:', error);
});
});
});
function updateStatus() {
const isOnline = Object.values(fakeStatus).some(status => status === 'online');
// Update main button based on any platform being online
const isOnline = Object.values(statusData).some(status => status === 'online');
statusButton.className = `status-button ${isOnline ? 'online' : 'offline'}`;
statusButton.textContent = isOnline ? 'GO OFFLINE' : 'GO ONLINE';
// Update individual platform buttons
statusMenu.querySelectorAll('button').forEach(btn => {
const platform = btn.dataset.platform;
btn.textContent = `${platform}: ${fakeStatus[platform]}`;
const statusDot = btn.querySelector('.platform-status');
const isOnline = statusData[platform] === 'online';
// Update status dot
statusDot.className = `platform-status ${statusData[platform]}`;
// Update button status attribute for styling
if (isOnline) {
btn.setAttribute('data-status', 'online');
} else {
btn.removeAttribute('data-status');
}
});
}
// Load panel content
const panelIframe = document.getElementById('panel-iframe');
panelIframe.srcdoc = '<p>Control panel content loaded here.</p>';
//const panelIframe = document.getElementById('panel-iframe');
//panelIframe.srcdoc = '<p>Control panel content loaded here.</p>';
// Chat functionality
const chatWindow = document.getElementById('chat-window');
......@@ -1000,18 +1189,18 @@
let completionState = { index: -1, prefix: '', original: '', matches: [] };
function getUsernames() {
const users = new Set();
for (const platform in fakeUsers) {
fakeUsers[platform].forEach(user => {
users.add(`${user.username}@${platform}`);
const usernames = new Set();
for (const platform in users) {
users[platform].forEach(user => {
usernames.add(`${user.username}@${platform}`);
});
}
fakeMessages.forEach(msg => {
messages.forEach(msg => {
if (msg.sender !== 'me' && !msg.sender.startsWith('system') && !msg.sender.startsWith('platform@')) {
users.add(msg.sender);
usernames.add(msg.sender);
}
});
return Array.from(users);
return Array.from(usernames);
}
chatInput.addEventListener('keydown', (e) => {
......@@ -1061,8 +1250,30 @@
const platform = platformSelector.value;
if (content) {
console.log(`Sending message to ${platform}: ${content}`);
fakeMessages.push({ sender: 'me', content, timestamp: new Date().toISOString() });
renderMessages(fakeMessages);
// Send message to server
fetch(`/api/send_message?client_id=${clientId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
platform: platform
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Add new message to the list
messages.push(data.message);
renderMessages(messages);
}
})
.catch(error => {
console.error('Error sending message:', error);
});
chatInput.value = '';
completionState = { index: -1, prefix: '', original: '', matches: [] };
}
......@@ -1100,9 +1311,9 @@
console.error(`Invalid platform.account format: ${platformAccount}`);
return;
}
const userList = fakeUsers[`${platform}.${account}`];
const userList = users[`${platform}.${account}`];
if (!userList) {
console.error(`Platform not found in fakeUsers: ${platform}.${account}`);
console.error(`Platform not found in users: ${platform}.${account}`);
return;
}
const user = userList.find(u => u.username === username);
......@@ -1149,14 +1360,14 @@
const totalUsers = document.getElementById('total-users');
let total = 0;
userList.innerHTML = '';
for (const platform in fakeUsers) {
total += fakeUsers[platform].length;
for (const platform in users) {
total += users[platform].length;
const platformDiv = document.createElement('div');
platformDiv.className = 'platform';
platformDiv.innerHTML = `<div class="platform-header">${platform} <span>(${fakeUsers[platform].length})</span></div>`;
platformDiv.innerHTML = `<div class="platform-header">${platform} <span>(${users[platform].length})</span></div>`;
const usersDiv = document.createElement('div');
usersDiv.className = 'users';
fakeUsers[platform].forEach(user => {
users[platform].forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'user';
userDiv.dataset.sender = `${user.username}@${platform}`;
......@@ -1186,20 +1397,35 @@
}
// Tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
});
});
document.querySelectorAll('.tabs').forEach(tabGroup => {
tabGroup.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
// Only affect tabs within the same tab group
tabGroup.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Only affect tab content elements related to this tab group
const tabContentId = tab.dataset.tab;
document.getElementById(tabContentId).classList.add('active');
// Find all sibling tab contents and hide them
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(content => {
if (content.id !== tabContentId &&
((tabGroup.closest('.left-column') && content.closest('.left-column')) ||
(tabGroup.closest('.right-column') && content.closest('.right-column')))) {
content.classList.remove('active');
}
});
});
});
});
// Earnings
function renderEarnings() {
const tbody = document.querySelector('.earnings-table tbody');
tbody.innerHTML = '';
const totals = fakeEarnings.reduce((acc, e) => ({
const totals = earnings.reduce((acc, e) => ({
lastSession: acc.lastSession + e.lastSession,
today: acc.today + e.today,
lastHour: acc.lastHour + e.lastHour,
......@@ -1215,7 +1441,7 @@
<td>$${totals.sess}</td>
`;
tbody.appendChild(totalsRow);
fakeEarnings.forEach(e => {
earnings.forEach(e => {
const [platform, account] = e.platform.split('.');
const row = document.createElement('tr');
row.innerHTML = `
......@@ -1231,15 +1457,29 @@
// Reset session button
document.getElementById('reset-session').addEventListener('click', () => {
fakeEarnings.forEach(e => e.sess = 0);
renderEarnings();
fetch('/api/reset_session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
// The long polling will automatically update the earnings data
console.log("Session reset successfully");
}
})
.catch(error => {
console.error('Error resetting session:', error);
});
});
// Private Chat functionality
const privateChatWindowsContainer = document.getElementById('private-chat-windows-container');
// Store private chat messages and states
const privateChats = {}; // Store chat messages by user
// Store private chat states
const openPrivateChats = new Set(); // Track open private chats
const activeChatWindows = {}; // Store references to active chat windows
......@@ -1269,16 +1509,30 @@
chatItem.className = 'private-chat-item';
chatItem.dataset.sender = sender;
// Create notification indicator
const notificationIndicator = document.createElement('span');
notificationIndicator.className = 'notification-indicator';
// Initially hide the notification indicator
notificationIndicator.style.display = 'none';
const nameSpan = document.createElement('div');
nameSpan.className = 'private-chat-item-name';
nameSpan.textContent = sender;
// Create a container for the indicator and name
const leftContainer = document.createElement('div');
leftContainer.style.display = 'flex';
leftContainer.style.alignItems = 'center';
leftContainer.appendChild(notificationIndicator);
leftContainer.appendChild(nameSpan);
const closeButton = document.createElement('div');
closeButton.className = 'private-chat-item-close';
closeButton.innerHTML = '&times;';
closeButton.title = 'Close chat';
chatItem.appendChild(nameSpan);
chatItem.appendChild(leftContainer);
chatItem.appendChild(closeButton);
// Click on chat item to open/restore the chat
......@@ -1325,7 +1579,10 @@
chatWindow.innerHTML = `
<div class="private-chat-content">
<div class="private-chat-header" data-sender="${sender}">
<div class="private-chat-title">Private Chat with <span class="private-chat-username">${sender}</span></div>
<div class="private-chat-title-container">
<span class="notification-indicator"></span>
<div class="private-chat-title">Private Chat with <span class="private-chat-username">${sender}</span></div>
</div>
<div class="private-chat-controls">
<span class="minimize-chat" title="Minimize">_</span>
<span class="close-chat" title="Close">&times;</span>
......@@ -1423,6 +1680,11 @@
showContextMenu(e, sender, true);
});
// Set active window on click (for z-index management)
chatWindow.addEventListener('mousedown', () => {
setActivePrivateChat(sender);
});
// Send button
sendBtn.addEventListener('click', () => {
sendPrivateMessage(sender);
......@@ -1464,6 +1726,81 @@
// Focus the input
chatWindow.querySelector('textarea').focus();
// Mark messages as read
markMessagesAsRead(sender);
// Set as active window
setActivePrivateChat(sender);
}
// Set active private chat window (manage z-index)
function setActivePrivateChat(sender) {
// Set all windows to z-index 990
Object.keys(activeChatWindows).forEach(key => {
if (activeChatWindows[key]) {
activeChatWindows[key].classList.remove('active');
}
});
// Set the clicked window to z-index 991
if (activeChatWindows[sender]) {
activeChatWindows[sender].classList.add('active');
}
}
// Mark messages as read for a specific sender
function markMessagesAsRead(sender) {
fetch(`/api/mark_messages_read?client_id=${clientId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sender: sender
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the notification indicator in the chat list
const chatItem = document.querySelector(`.private-chat-item[data-sender="${sender}"]`);
if (chatItem) {
const indicator = chatItem.querySelector('.notification-indicator');
if (indicator) {
indicator.style.display = 'none';
}
}
// Also hide the indicator in the chat window header
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
const headerIndicator = chatWindow.querySelector('.private-chat-header .notification-indicator');
if (headerIndicator) {
headerIndicator.style.display = 'none';
}
}
// Check if any other chats have unread messages
let anyUnread = false;
document.querySelectorAll('.private-chat-item .notification-indicator').forEach(ind => {
if (ind.style.display === 'inline-block') {
anyUnread = true;
}
});
// Update tab indicator
if (!anyUnread) {
const tabIndicator = document.getElementById('private-chats-tab-indicator');
if (tabIndicator) {
tabIndicator.style.display = 'none';
}
}
}
})
.catch(error => {
console.error('Error marking messages as read:', error);
});
}
// Close a private chat window
......@@ -1541,59 +1878,428 @@
const content = textarea.value.trim();
if (content) {
// Add message to private chat
if (!privateChats[sender]) {
privateChats[sender] = [];
// Send private message to server
fetch(`/api/send_private_message?client_id=${clientId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: sender,
content: content
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update private chat messages
privateChats[sender] = data.messages;
renderPrivateMessages(sender);
}
})
.catch(error => {
console.error('Error sending private message:', error);
});
textarea.value = '';
}
}
// Panel expand/collapse functionality
const panelExpandToggle = document.getElementById('panel-expand-toggle');
const panelIframe = document.getElementById('panel-iframe');
const panelContainer = document.querySelector('.panel-container');
let isPanelExpanded = false;
panelExpandToggle.addEventListener('click', () => {
isPanelExpanded = !isPanelExpanded;
if (isPanelExpanded) {
// Expand panel
panelContainer.style.position = 'fixed';
panelContainer.style.top = '10%';
panelContainer.style.left = '10%';
panelContainer.style.width = '80%';
panelContainer.style.height = '80%';
panelContainer.style.zIndex = '1010';
panelIframe.style.height = '100%';
panelExpandToggle.innerHTML = '⤓';
panelContainer.style.background = '#1c1c1c';
panelContainer.style.boxShadow = '0 0 20px rgba(0, 0, 0, 0.5)';
panelContainer.style.borderRadius = '8px';
} else {
// Collapse panel
panelContainer.style.position = 'relative';
panelContainer.style.top = 'auto';
panelContainer.style.left = 'auto';
panelContainer.style.width = '100%';
panelContainer.style.height = 'auto';
panelContainer.style.zIndex = 'auto';
panelIframe.style.height = '300px';
panelExpandToggle.innerHTML = '⤢';
panelContainer.style.background = 'none';
panelContainer.style.boxShadow = 'none';
}
});
// Client ID for tracking requests
const clientId = `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
let lastRequestTime = 0;
/**
* Hybrid real-time data synchronization system
*
* This implements a dual approach for real-time updates:
* 1. Primary: SocketIO for immediate push notifications when data changes
* 2. Fallback: Long polling with adaptive backoff for reliability
*
* The combination ensures:
* - Immediate updates when possible (via SocketIO)
* - Reliability in all network conditions (via long polling)
* - Efficient server resource usage (non-blocking on server, adaptive polling on client)
* - Multiple browser windows can open the chat page simultaneously
*/
async function fetchData() {
try {
// Initial data load
await fetchChatData();
// Ensure UI is updated with initial data
renderMessages(messages);
renderUserList();
renderEarnings();
updateStatus();
renderPrivateChatslist();
// Start long polling as a fallback mechanism
// SocketIO will be the primary update mechanism
startLongPolling();
} catch (error) {
console.error("Error fetching data:", error);
// Retry after a short delay
setTimeout(fetchData, 5000);
}
}
// Fetch chat data from the consolidated API endpoint
async function fetchChatData() {
try {
const response = await fetch(`/api/chat?client_id=${clientId}&last_request=${lastRequestTime}`);
const data = await response.json();
// Update last request time for next poll
lastRequestTime = data.timestamp;
// If there are no updates, just return the data for retry handling
if (data.no_update) {
return data;
}
// Update all data
messages = data.messages || [];
users = data.users || {};
earnings = data.earnings || [];
statusData = data.status || {};
rtspUrls = data.rtsp_urls || [];
privateChats = data.private_chats || {};
// Initialize private chats if they exist in the data
if (privateChats) {
for (const sender in privateChats) {
if (privateChats[sender] && privateChats[sender].length > 0) {
// Add to open chats if not already there
if (!openPrivateChats.has(sender)) {
openPrivateChats.add(sender);
}
}
}
}
// Handle unread private messages
if (data.unread_private_messages) {
updateUnreadIndicators(data.unread_private_messages);
// Handle unread messages
for (const sender in data.unread_private_messages) {
if (data.unread_private_messages[sender] > 0 && privateChats[sender] && privateChats[sender].length > 0) {
// Add to open chats if not already there
if (!openPrivateChats.has(sender)) {
openPrivateChats.add(sender);
// Open a new chat window if auto-open is enabled
if (autoOpenPrivateChats && !activeChatWindows[sender]) {
openPrivateChat(sender);
} else if (!activeChatWindows[sender] || activeChatWindows[sender].style.display === 'none') {
// Show notification indicator in the chat list
const chatItem = document.querySelector(`.private-chat-item[data-sender="${sender}"]`);
if (chatItem) {
const indicator = chatItem.querySelector('.notification-indicator');
if (indicator) {
indicator.style.display = 'inline-block';
}
}
}
}
}
}
}
// Always render the private chats list
renderPrivateChatslist();
// Update UI
renderMessages(messages);
renderUserList();
renderEarnings();
updateStatus();
// Update private chats list
renderPrivateChatslist();
return data;
} catch (error) {
console.error("Error fetching chat data:", error);
throw error;
}
}
// Adaptive polling with exponential backoff
let pollBackoffDelay = 1000; // Start with 1 second
const maxPollBackoffDelay = 10000; // Max 10 seconds
// Start long polling for updates
function startLongPolling() {
fetchChatData()
.then(data => {
if (data && data.no_update) {
// No updates available, use adaptive backoff
pollBackoffDelay = Math.min(pollBackoffDelay * 1.5, maxPollBackoffDelay);
const retryAfter = data.retry_after || pollBackoffDelay;
console.log(`No updates, next poll in ${retryAfter}ms`);
setTimeout(startLongPolling, retryAfter);
} else {
// Got updates, reset backoff and immediately start the next poll
pollBackoffDelay = 1000;
startLongPolling();
}
})
.catch(error => {
console.error("Long polling error:", error);
// Retry after a short delay
setTimeout(startLongPolling, 5000);
});
}
// Initialize SocketIO connection with reconnection options
const socket = io({
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000
});
// Listen for chat updates
socket.on('chat_update', (data) => {
console.log('Received update notification:', data);
// If it's a private chat update with specific sender info
if (data.type === 'private_chats' && data.sender) {
// Fetch the latest data with priority
fetchChatData().then(responseData => {
// If this update indicates unread messages
if (data.has_unread) {
const sender = data.sender;
// Add to open chats if not already there
if (!openPrivateChats.has(sender)) {
openPrivateChats.add(sender);
renderPrivateChatslist();
}
// Show indicator in the tab title
const tabIndicator = document.getElementById('private-chats-tab-indicator');
if (tabIndicator) {
tabIndicator.style.display = 'inline-block';
}
// Check if we should auto-open or just show notification
const chatWindow = activeChatWindows[sender];
if (!chatWindow) {
if (autoOpenPrivateChats) {
// Create and open a new chat window if auto-open is enabled
openPrivateChat(sender);
} else {
// Just show notification in the chat list
const chatItem = document.querySelector(`.private-chat-item[data-sender="${sender}"]`);
if (chatItem) {
const indicator = chatItem.querySelector('.notification-indicator');
if (indicator) {
indicator.style.display = 'inline-block';
}
}
}
} else if (chatWindow.style.display === 'none') {
// Window exists but is minimized, show notification in chat list
const chatItem = document.querySelector(`.private-chat-item[data-sender="${sender}"]`);
if (chatItem) {
const indicator = chatItem.querySelector('.notification-indicator');
if (indicator) {
indicator.style.display = 'inline-block';
}
}
} else if (!chatWindow.classList.contains('active')) {
// Window is open but not focused, show indicator in header
const headerIndicator = chatWindow.querySelector('.private-chat-header .notification-indicator');
if (headerIndicator) {
headerIndicator.style.display = 'inline-block';
}
}
// Update the chat window content if it exists
if (chatWindow && privateChats[sender]) {
renderPrivateMessages(sender);
}
} else if (data.has_unread === false) {
// This is a "mark as read" update
const sender = data.sender;
privateChats[sender].push({
sender: 'me',
content: content,
timestamp: new Date().toISOString()
// Hide notification indicators
const chatItem = document.querySelector(`.private-chat-item[data-sender="${sender}"]`);
if (chatItem) {
const indicator = chatItem.querySelector('.notification-indicator');
if (indicator) {
indicator.style.display = 'none';
}
}
// Check if any other chats have unread messages
let anyUnread = false;
document.querySelectorAll('.private-chat-item .notification-indicator').forEach(ind => {
if (ind.style.display === 'inline-block') {
anyUnread = true;
}
});
// Simulate receiving a response after a short delay
setTimeout(() => {
privateChats[sender].push({
sender: sender,
content: `Thanks for your message: "${content}"`,
timestamp: new Date().toISOString()
});
if (activeChatWindows[sender] && activeChatWindows[sender].style.display === 'block') {
renderPrivateMessages(sender);
// Update tab indicator
if (!anyUnread) {
const tabIndicator = document.getElementById('private-chats-tab-indicator');
if (tabIndicator) {
tabIndicator.style.display = 'none';
}
}, 1000);
}
renderPrivateMessages(sender);
textarea.value = '';
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
const headerIndicator = chatWindow.querySelector('.private-chat-header .notification-indicator');
if (headerIndicator) {
headerIndicator.style.display = 'none';
}
}
}
});
} else if (data.type === 'initial_connect') {
// New connection - fetch all data immediately
fetchChatData();
} else {
// For other updates, just fetch the data
fetchChatData();
}
});
// Handle connection events
socket.on('connect', () => {
console.log('SocketIO connected');
});
socket.on('disconnect', () => {
console.log('SocketIO disconnected');
});
socket.on('connect_error', (error) => {
console.error('SocketIO connection error:', error);
});
// Function to update unread message indicators
function updateUnreadIndicators(unreadCounts) {
// If unreadCounts is undefined or null, return early
if (!unreadCounts) return;
// First ensure all private chats are in the list
let hasAnyUnread = false;
for (const sender in unreadCounts) {
if (unreadCounts[sender] > 0 && privateChats[sender] && privateChats[sender].length > 0) {
// Add to open chats if not already there
if (!openPrivateChats.has(sender)) {
openPrivateChats.add(sender);
renderPrivateChatslist();
}
hasAnyUnread = true;
}
}
// Update the tab indicator based on whether any chats have unread messages
const tabIndicator = document.getElementById('private-chats-tab-indicator');
if (tabIndicator) {
tabIndicator.style.display = hasAnyUnread ? 'inline-block' : 'none';
}
// Update indicators in the private chats list
document.querySelectorAll('.private-chat-item').forEach(item => {
const sender = item.dataset.sender;
const indicator = item.querySelector('.notification-indicator');
if (indicator) {
if (unreadCounts[sender] && unreadCounts[sender] > 0) {
// Show indicator in chat list
indicator.style.display = 'inline-block';
hasAnyUnread = true;
// Also show indicator in chat window header if window exists and is not active
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
if (chatWindow.style.display === 'none' || !chatWindow.classList.contains('active')) {
const headerIndicator = chatWindow.querySelector('.private-chat-header .notification-indicator');
if (headerIndicator) {
headerIndicator.style.display = 'inline-block';
}
}
}
} else {
// Hide indicator
indicator.style.display = 'none';
// Also hide in header
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
const headerIndicator = chatWindow.querySelector('.private-chat-header .notification-indicator');
if (headerIndicator) {
headerIndicator.style.display = 'none';
}
}
}
}
});
}
// Initialize
populateVideoSources();
renderMessages(fakeMessages);
renderUserList();
renderEarnings();
updateStatus();
fetchData();
// Create a test private chat for demonstration
const testSender = "alice@C4.sexhackme";
privateChats[testSender] = [
{
sender: 'me',
content: 'Hello Alice!',
timestamp: new Date().toISOString()
},
{
sender: testSender,
content: 'Hi there! How can I help you?',
timestamp: new Date().toISOString()
}
];
openPrivateChats.add(testSender);
renderPrivateChatslist();
// Auto-open toggle event listener
document.getElementById('auto-open-chats').addEventListener('change', function() {
autoOpenPrivateChats = this.checked;
console.log(`Auto-open private chats: ${autoOpenPrivateChats ? 'enabled' : 'disabled'}`);
// Save preference to localStorage
localStorage.setItem('autoOpenPrivateChats', autoOpenPrivateChats);
});
// Load auto-open preference from localStorage
if (localStorage.getItem('autoOpenPrivateChats') !== null) {
autoOpenPrivateChats = localStorage.getItem('autoOpenPrivateChats') === 'true';
document.getElementById('auto-open-chats').checked = autoOpenPrivateChats;
}
// Handle window resize to keep chat windows within viewport
window.addEventListener('resize', () => {
......
<!--
Copyright (C) 2023 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streaming Control Panel</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body, html {
height: 100%;
font-family: Arial, sans-serif;
overflow: hidden;
}
.main-container {
display: flex;
height: 100vh;
width: 100vw;
}
.buttons-container {
display: grid;
grid-template-columns: repeat(120, 1fr);
grid-template-rows: repeat(4, 1fr);
height: 100%;
width: 100%;
}
.button_row {
display: grid;
grid-template-columns: repeat(120, 1fr);
width: 100%;
grid-column: span 120;
}
{{style_rows|safe}}
.button {
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5vw;
font-weight: bold;
text-align: center;
border: 2px solid white;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
}
/* Color variations */
.private { background-color: #4CAF50; color: white; }
.toggle { background-color: #2196F3; color: white; }
.special { background-color: #FF9800; color: white; }
.button:hover {
opacity: 0.8;
transform: scale(1.05);
}
.button:active {
background-color: #45a049;
transform: scale(0.95);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.button {
font-size: 3vw;
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js" integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO" crossorigin="anonymous"></script>
</head>
<body>
<div class="main-container">
<!-- Buttons Container -->
<div class="buttons-container">
{{htmlbuttons|safe}}
</div>
</div>
<script>
function executeCommand(command) {
fetch('/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `command=${command}`
})
.then(response => response.text())
.then(result => {
console.log(result);
// Visual feedback
/*event.target.style.backgroundColor = '#45a049';
setTimeout(() => {
event.target.style.backgroundColor =
event.target.classList.contains('private') ? '#4CAF50' :
event.target.classList.contains('toggle') ? '#2196F3' :
'#FF9800';
}, 300); */
})
.catch(error => {
console.error('Error:', error);
});
}
// Prevent zoom on double-tap for mobile
document.addEventListener('touchmove', function(event) {
if (event.scale !== 1) { event.preventDefault(); }
}, { passive: false });
$(document).ready(function() {
// Connect to the Socket.IO server.
// The connection URL has the following format, relative to the current page:
// http[s]://<domain>:<port>[/<namespace>]
var socket = io();
// Event handler for new connections.
// The callback function is invoked when a connection with the
// server is established.
socket.on('connect', function() {
socket.emit('my_event', {data: 'I\'m connected!'});
});
// Event handler for server sent data.
// The callback function is invoked whenever the server emits data
// to the client. The data is then displayed in the "Received"
// section of the page.
socket.on('my_response', function(msg, cb) {
$('#log').append('<br>' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html());
if (cb)
cb();
});
// Interval function that tests message latency by sending a "ping"
// message. The server then responds with a "pong" message and the
// round trip time is measured.
var ping_pong_times = [];
var start_time;
window.setInterval(function() {
start_time = (new Date).getTime();
$('#transport').text(socket.io.engine.transport.name);
socket.emit('my_ping');
}, 1000);
// Handler for the "pong" message. When the pong is received, the
// time from the ping is stored, and the average of the last 30
// samples is average and displayed.
socket.on('my_pong', function() {
var latency = (new Date).getTime() - start_time;
ping_pong_times.push(latency);
ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples
var sum = 0;
for (var i = 0; i < ping_pong_times.length; i++)
sum += ping_pong_times[i];
$('#ping-pong').text(Math.round(10 * sum / ping_pong_times.length) / 10);
});
window.setInterval(function() {
socket.emit('get_queue');
}, 200);
socket.on('change_output', function(msg, cb) {
//console.log('CHANGE OUTPUT');
//console.log(msg);
$('.output_'+msg.button).css('background-color', msg.color);
});
socket.on('change_input', function(msg, cb) {
$('.input_'+msg.button).css('background-color', msg.color);
});
socket.on('change_feedback', function(msg, cb) {
if(msg.hasOwnProperty("color"))
$('.feedback_'+msg.feedback).css('background-color', msg.color);
if(msg.hasOwnProperty("title"))
$('.feedback_'+msg.feedback).text(msg.title);
});
});
</script>
<div id="log"></div>
<div id="ping-pong"></div>ms</b>
<div id="transport"></div>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment