640 lines
22 KiB
C++
Raw Normal View History

#include <QApplication>
#include <QMainWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QColorDialog>
#include <QSlider>
#include <QSpinBox>
#include <QSslSocket>
#include <QHostAddress>
#include <QMessageBox>
#include <QThread>
#include <QTabWidget>
#include <QCamera>
#include <QCameraInfo>
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QScrollArea>
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QDialog>
#include <iostream>
#include <mutex>
#include <map>
class VideoFeedWidget : public QWidget {
Q_OBJECT
public:
VideoFeedWidget(const QString &username, QWidget *parent = nullptr)
: QWidget(parent), m_username(username) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(5, 5, 5, 5);
// Title
QLabel *title = new QLabel(m_username + " (Offline)", this);
title->setStyleSheet("font-weight: bold; font-size: 12px;");
layout->addWidget(title);
// Video placeholder
m_video_label = new QLabel(this);
m_video_label->setMinimumSize(320, 240);
m_video_label->setStyleSheet("border: 2px solid gray; background-color: black;");
m_video_label->setAlignment(Qt::AlignCenter);
m_video_label->setText("Waiting for video...");
layout->addWidget(m_video_label);
setStyleSheet("border: 1px solid gray; padding: 5px;");
}
void update_video_frame(const QPixmap &pixmap) {
m_video_label->setPixmap(pixmap.scaled(320, 240, Qt::KeepAspectRatio));
}
void set_status(bool online) {
QLabel *title = findChild<QLabel *>();
if (title) {
title->setText(m_username + (online ? " (Online)" : " (Offline)"));
}
}
private:
QString m_username;
QLabel *m_video_label;
};
// Login dialog for authentication
class LoginDialog : public QDialog {
Q_OBJECT
public:
LoginDialog(QWidget *parent = nullptr) : QDialog(parent) {
setWindowTitle("SCAR Chat - Login");
setModal(true);
setGeometry(100, 100, 400, 300);
QVBoxLayout *layout = new QVBoxLayout(this);
// Title
QLabel *title = new QLabel("SCAR Chat", this);
title->setStyleSheet("font-size: 24px; font-weight: bold;");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
layout->addSpacing(20);
// Server address
QHBoxLayout *server_layout = new QHBoxLayout();
QLabel *server_label = new QLabel("Server:", this);
server_input = new QLineEdit("localhost", this);
server_layout->addWidget(server_label);
server_layout->addWidget(server_input);
layout->addLayout(server_layout);
// Server port
QHBoxLayout *port_layout = new QHBoxLayout();
QLabel *port_label = new QLabel("Port:", this);
port_input = new QLineEdit("42317", this);
port_input->setMaximumWidth(100);
port_layout->addWidget(port_label);
port_layout->addWidget(port_input);
port_layout->addStretch();
layout->addLayout(port_layout);
layout->addSpacing(15);
// Username
QHBoxLayout *user_layout = new QHBoxLayout();
QLabel *user_label = new QLabel("Username:", this);
username_input = new QLineEdit(this);
user_layout->addWidget(user_label);
user_layout->addWidget(username_input);
layout->addLayout(user_layout);
// Password
QHBoxLayout *pass_layout = new QHBoxLayout();
QLabel *pass_label = new QLabel("Password:", this);
password_input = new QLineEdit(this);
password_input->setEchoMode(QLineEdit::Password);
pass_layout->addWidget(pass_label);
pass_layout->addWidget(password_input);
layout->addLayout(pass_layout);
layout->addSpacing(15);
// Status label
status_label = new QLabel("", this);
status_label->setAlignment(Qt::AlignCenter);
status_label->setStyleSheet("color: #666;");
layout->addWidget(status_label);
// Buttons
QHBoxLayout *btn_layout = new QHBoxLayout();
login_btn = new QPushButton("Login", this);
cancel_btn = new QPushButton("Cancel", this);
btn_layout->addStretch();
btn_layout->addWidget(login_btn);
btn_layout->addWidget(cancel_btn);
layout->addLayout(btn_layout);
connect(login_btn, &QPushButton::clicked, this, &LoginDialog::accept);
connect(cancel_btn, &QPushButton::clicked, this, &LoginDialog::reject);
connect(password_input, &QLineEdit::returnPressed, this, &LoginDialog::accept);
}
QString get_server() const { return server_input->text(); }
int get_port() const { return port_input->text().toInt(); }
QString get_username() const { return username_input->text(); }
QString get_password() const { return password_input->text(); }
void set_status(const QString &msg) { status_label->setText(msg); }
void set_login_enabled(bool enabled) { login_btn->setEnabled(enabled); }
private:
QLineEdit *server_input;
QLineEdit *port_input;
QLineEdit *username_input;
QLineEdit *password_input;
QLabel *status_label;
QPushButton *login_btn;
QPushButton *cancel_btn;
};
class ChatClient : public QMainWindow {
Q_OBJECT
public:
ChatClient(QWidget *parent = nullptr) : QMainWindow(parent), m_camera(nullptr), m_authenticated(false) {
setWindowTitle("SCAR Chat Client");
setGeometry(100, 100, 1000, 700);
// Create central widget with tabs
QWidget *central = new QWidget(this);
setCentralWidget(central);
QVBoxLayout *main_layout = new QVBoxLayout(central);
// Status bar showing logged-in user
status_label = new QLabel("Not authenticated", this);
status_label->setStyleSheet("font-weight: bold;");
main_layout->addWidget(status_label);
// Tab widget for chat and video
QTabWidget *tabs = new QTabWidget(this);
// ==================== CHAT TAB ====================
QWidget *chat_tab = new QWidget(this);
QVBoxLayout *chat_layout = new QVBoxLayout(chat_tab);
// Chat display
chat_display = new QTextEdit(this);
chat_display->setReadOnly(true);
chat_layout->addWidget(chat_display);
// Message input
QHBoxLayout *msg_layout = new QHBoxLayout();
message_input = new QLineEdit(this);
send_btn = new QPushButton("Send", this);
msg_layout->addWidget(message_input);
msg_layout->addWidget(send_btn);
chat_layout->addLayout(msg_layout);
// Chat settings
QHBoxLayout *chat_settings = new QHBoxLayout();
bg_color_btn = new QPushButton("BG Color", this);
text_color_btn = new QPushButton("Text Color", this);
QLabel *trans_label = new QLabel("Transparency:", this);
transparency_slider = new QSlider(Qt::Horizontal, this);
transparency_slider->setMinimum(0);
transparency_slider->setMaximum(100);
transparency_slider->setValue(100);
transparency_value = new QLabel("100%", this);
chat_settings->addWidget(bg_color_btn);
chat_settings->addWidget(text_color_btn);
chat_settings->addWidget(trans_label);
chat_settings->addWidget(transparency_slider);
chat_settings->addWidget(transparency_value);
chat_layout->addLayout(chat_settings);
tabs->addTab(chat_tab, "Chat");
// ==================== VIDEO TAB ====================
QWidget *video_tab = new QWidget(this);
QVBoxLayout *video_layout = new QVBoxLayout(video_tab);
// Camera controls
QHBoxLayout *camera_ctrl = new QHBoxLayout();
camera_combo = new QComboBox(this);
camera_toggle = new QCheckBox("Enable Camera", this);
camera_toggle->setChecked(false);
camera_info_label = new QLabel("No camera selected", this);
camera_ctrl->addWidget(new QLabel("Camera:", this));
camera_ctrl->addWidget(camera_combo);
camera_ctrl->addWidget(camera_toggle);
camera_ctrl->addWidget(camera_info_label);
camera_ctrl->addStretch();
video_layout->addLayout(camera_ctrl);
// Local camera view
QLabel *local_title = new QLabel("Your Camera Feed:", this);
local_title->setStyleSheet("font-weight: bold;");
video_layout->addWidget(local_title);
local_video_label = new QLabel(this);
local_video_label->setMinimumSize(640, 480);
local_video_label->setMaximumSize(640, 480);
local_video_label->setStyleSheet("border: 2px solid gray; background-color: black;");
local_video_label->setAlignment(Qt::AlignCenter);
local_video_label->setText("Camera Disabled\nClick 'Enable Camera' to start");
video_layout->addWidget(local_video_label, 0, Qt::AlignHCenter);
// Remote video feeds
QLabel *remote_title = new QLabel("Connected Users:", this);
remote_title->setStyleSheet("font-weight: bold; margin-top: 20px;");
video_layout->addWidget(remote_title);
// Scroll area for remote feeds
QScrollArea *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
QWidget *scroll_content = new QWidget(this);
remote_feeds_layout = new QGridLayout(scroll_content);
scroll->setWidget(scroll_content);
video_layout->addWidget(scroll);
tabs->addTab(video_tab, "Video Feeds");
main_layout->addWidget(tabs);
// Connect signals
connect(send_btn, &QPushButton::clicked, this, &ChatClient::on_send_message);
connect(message_input, &QLineEdit::returnPressed, this, &ChatClient::on_send_message);
connect(bg_color_btn, &QPushButton::clicked, this, &ChatClient::on_bg_color);
connect(text_color_btn, &QPushButton::clicked, this, &ChatClient::on_text_color);
connect(transparency_slider, &QSlider::valueChanged, this, &ChatClient::on_transparency_changed);
connect(camera_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ChatClient::on_camera_changed);
connect(camera_toggle, &QCheckBox::stateChanged, this, &ChatClient::on_camera_toggle);
// Initialize socket
socket = new QSslSocket(this);
connect(socket, &QSslSocket::connected, this, &ChatClient::on_socket_connected);
connect(socket, &QSslSocket::disconnected, this, &ChatClient::on_socket_disconnected);
connect(socket, &QSslSocket::readyRead, this, &ChatClient::on_socket_read);
connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QSslSocket::errorOccurred),
this, &ChatClient::on_socket_error);
connect(socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
this, &ChatClient::on_ssl_errors);
// Initialize colors
bg_color = Qt::white;
text_color = Qt::black;
update_window_colors();
// Populate camera list
populate_cameras();
chat_display->append("[System] SCAR Chat Client started.");
chat_display->append("[Info] Camera detection active.");
chat_display->append("[Info] Select a camera and enable it to start video capture.");
// Show login dialog
show_login_dialog();
}
~ChatClient() {
if (m_camera) {
m_camera->stop();
delete m_camera;
}
}
private slots:
void show_login_dialog() {
LoginDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
QString server = dialog.get_server();
int port = dialog.get_port();
QString username = dialog.get_username();
QString password = dialog.get_password();
if (server.isEmpty() || username.isEmpty() || password.isEmpty()) {
QMessageBox::warning(this, "Error", "Please fill in all fields");
show_login_dialog();
return;
}
chat_display->append("[System] Connecting to " + server + ":" + QString::number(port) + "...");
m_current_username = username;
m_current_password = password;
m_login_dialog_server = server;
m_login_dialog_port = port;
socket->connectToHostEncrypted(server, port);
} else {
// User cancelled login
QApplication::quit();
}
}
void populate_cameras() {
camera_combo->clear();
const QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
if (cameras.isEmpty()) {
camera_combo->addItem("No camera detected");
camera_toggle->setEnabled(false);
camera_info_label->setText("No cameras found on this system");
chat_display->append("[Warning] No cameras detected on system");
} else {
for (int i = 0; i < cameras.size(); ++i) {
const QCameraInfo &camera = cameras.at(i);
QString device_name = camera.deviceName();
QString description = camera.description();
QString label = description.isEmpty() ? ("Camera " + QString::number(i)) : description;
camera_combo->addItem(label, device_name);
}
camera_toggle->setEnabled(true);
if (!cameras.isEmpty()) {
camera_info_label->setText("Available cameras: " + QString::number(cameras.size()));
chat_display->append("[System] Found " + QString::number(cameras.size()) + " camera(s)");
for (int i = 0; i < cameras.size(); ++i) {
chat_display->append(" [" + QString::number(i + 1) + "] " + cameras.at(i).description());
}
}
}
}
void on_camera_changed(int index) {
if (index < 0) return;
if (m_camera && camera_toggle->isChecked()) {
m_camera->stop();
}
QString device_name = camera_combo->currentData().toString();
if (device_name.isEmpty()) {
camera_info_label->setText("Invalid camera selection");
return;
}
QCameraInfo camera_info(device_name.toLatin1());
if (m_camera) {
delete m_camera;
}
m_camera = new QCamera(camera_info, this);
QString cam_desc = camera_info.description();
if (cam_desc.isEmpty()) {
cam_desc = camera_combo->currentText();
}
camera_info_label->setText("Selected: " + cam_desc);
chat_display->append("[System] Camera changed to: " + cam_desc);
if (camera_toggle->isChecked()) {
start_camera();
}
}
void on_camera_toggle(int state) {
if (state == Qt::Checked) {
start_camera();
} else {
stop_camera();
}
}
void start_camera() {
if (!m_camera) {
chat_display->append("[Error] No camera selected");
camera_toggle->setChecked(false);
return;
}
try {
m_camera->start();
chat_display->append("[System] Camera enabled: " + camera_info_label->text());
local_video_label->setText("Camera Active\n(Video capture enabled)");
local_video_label->setStyleSheet("border: 2px solid green; background-color: #1a1a1a; color: white;");
if (socket->state() == QSslSocket::ConnectedState) {
socket->write("CAMERA_ENABLE\n");
}
} catch (const std::exception &e) {
chat_display->append("[Error] Failed to start camera: " + QString(e.what()));
camera_toggle->setChecked(false);
}
}
void stop_camera() {
if (m_camera) {
m_camera->stop();
}
chat_display->append("[System] Camera disabled");
local_video_label->setText("Camera Disabled\nClick 'Enable Camera' to start");
local_video_label->setStyleSheet("border: 2px solid gray; background-color: black; color: white;");
if (socket->state() == QSslSocket::ConnectedState) {
socket->write("CAMERA_DISABLE\n");
}
}
void on_socket_connected() {
chat_display->append("[System] Connected to server!");
// Send LOGIN command
QString login_cmd = "LOGIN:" + m_current_username + ":" + m_current_password;
socket->write(login_cmd.toUtf8() + "\n");
message_input->setFocus();
}
void on_socket_disconnected() {
chat_display->append("[System] Disconnected from server");
if (camera_toggle->isChecked()) {
camera_toggle->setChecked(false);
}
stop_camera();
if (m_authenticated) {
QMessageBox::information(this, "Disconnected", "You have been disconnected from the server");
show_login_dialog();
}
}
void on_socket_read() {
// Read all available data
QByteArray data = socket->readAll();
if (data.isEmpty()) return;
// Append to buffer
m_read_buffer.append(data);
// Process complete lines
while (true) {
int newline_pos = m_read_buffer.indexOf('\n');
if (newline_pos == -1) break; // No complete line yet
QString message = QString::fromUtf8(m_read_buffer.left(newline_pos)).trimmed();
m_read_buffer.remove(0, newline_pos + 1);
if (message.isEmpty()) continue; // Skip empty lines
// Handle authentication response
if (message.startsWith("LOGIN_SUCCESS:")) {
QString username = message.mid(14);
m_authenticated = true;
status_label->setText("Logged in as: " + username);
chat_display->append("[System] Successfully logged in as " + username);
} else if (message.startsWith("LOGIN_FAILED:")) {
QString reason = message.mid(13);
m_authenticated = false;
chat_display->append("[System] Login failed: " + reason);
socket->disconnectFromHost();
QMessageBox::critical(this, "Login Failed", "Login failed: " + reason);
show_login_dialog();
} else if (message.startsWith("ERROR:")) {
QString error = message.mid(6);
chat_display->append("[Error] " + error);
} else if (message.startsWith("USER_CAMERA_ON:")) {
QString username = message.mid(15);
add_remote_user(username, true);
chat_display->append("[System] " + username + " enabled their camera");
} else if (message.startsWith("USER_CAMERA_OFF:")) {
QString username = message.mid(16);
add_remote_user(username, false);
chat_display->append("[System] " + username + " disabled their camera");
} else if (message.startsWith("VIDEO_FRAME:")) {
// Placeholder for future video frame implementation
} else {
// Regular chat message
chat_display->append(message);
}
}
}
void on_socket_error(QAbstractSocket::SocketError error) {
Q_UNUSED(error);
chat_display->append("[Error] " + socket->errorString());
}
void on_ssl_errors(const QList<QSslError> &errors) {
for (const QSslError &error : errors) {
std::cerr << "SSL Error: " << error.errorString().toStdString() << std::endl;
}
socket->ignoreSslErrors();
}
void on_send_message() {
if (socket->state() != QSslSocket::ConnectedState) {
chat_display->append("[System] Not connected!");
return;
}
QString message = message_input->text();
if (message.isEmpty()) return;
socket->write(message.toUtf8() + "\n");
chat_display->append("[You] " + message);
message_input->clear();
}
void on_bg_color() {
QColor color = QColorDialog::getColor(bg_color, this, "Select Background Color");
if (color.isValid()) {
bg_color = color;
update_window_colors();
}
}
void on_text_color() {
QColor color = QColorDialog::getColor(text_color, this, "Select Text Color");
if (color.isValid()) {
text_color = color;
update_window_colors();
}
}
void on_transparency_changed(int value) {
transparency_value->setText(QString::number(value) + "%");
double opacity = value / 100.0;
setWindowOpacity(opacity);
}
void add_remote_user(const QString &username, bool camera_active) {
if (remote_users.find(username) == remote_users.end()) {
VideoFeedWidget *widget = new VideoFeedWidget(username, this);
remote_users[username] = widget;
remote_feeds_layout->addWidget(widget);
}
remote_users[username]->set_status(camera_active);
}
private:
void update_window_colors() {
QString stylesheet = QString(
"QTextEdit { background-color: %1; color: %2; }"
"QLineEdit { background-color: %1; color: %2; }"
"QPushButton { background-color: %1; color: %2; border: 1px solid gray; padding: 4px; }"
"QLabel { color: %2; }"
"QSlider { color: %2; }"
"QComboBox { background-color: %1; color: %2; }"
"QCheckBox { color: %2; }"
).arg(bg_color.name()).arg(text_color.name());
setStyleSheet(stylesheet);
}
private:
// Chat UI
QLabel *status_label;
QTextEdit *chat_display;
QLineEdit *message_input;
QPushButton *send_btn;
QPushButton *bg_color_btn;
QPushButton *text_color_btn;
QSlider *transparency_slider;
QLabel *transparency_value;
// Video UI
QComboBox *camera_combo;
QCheckBox *camera_toggle;
QLabel *camera_info_label;
QLabel *local_video_label;
QGridLayout *remote_feeds_layout;
std::map<QString, VideoFeedWidget *> remote_users;
// Camera
QCamera *m_camera;
// Connection
QSslSocket *socket;
QByteArray m_read_buffer;
QColor bg_color;
QColor text_color;
// Authentication
bool m_authenticated;
QString m_current_username;
QString m_current_password;
QString m_login_dialog_server;
int m_login_dialog_port;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
ChatClient client;
client.show();
return app.exec();
}
#include "main.moc"