first commit
@@ -0,0 +1,91 @@
|
||||
# Игнорировать пользовательские файлы и каталоги
|
||||
*.user
|
||||
*.user.*
|
||||
*.suo
|
||||
*.ncb
|
||||
*.sdf
|
||||
*.opensdf
|
||||
*.log
|
||||
*.idb
|
||||
*.pdb
|
||||
*.obj
|
||||
*.pch
|
||||
*.sln
|
||||
*.vcxproj.*
|
||||
*.vcxproj.user
|
||||
|
||||
# Собранные файлы и каталоги
|
||||
build-*/
|
||||
build/
|
||||
Debug/
|
||||
Release/
|
||||
*.exe
|
||||
*.app
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Qt Creator файлы
|
||||
.cproject
|
||||
.project
|
||||
.autotools
|
||||
*.autosave
|
||||
|
||||
# Скомпилированные интерфейсы
|
||||
ui_*.h
|
||||
moc_*.cpp
|
||||
moc_*.h
|
||||
qrc_*.cpp
|
||||
|
||||
# Метаобъектный код
|
||||
moc_*.cpp
|
||||
moc_*.h
|
||||
|
||||
# Ресурсы
|
||||
qrc_*.cpp
|
||||
|
||||
# Переводы
|
||||
*.qm
|
||||
|
||||
# Файлы сборки Qt
|
||||
Makefile*
|
||||
*.o
|
||||
*.o.d
|
||||
*.a
|
||||
*.la
|
||||
*.lib
|
||||
*.dll.a
|
||||
|
||||
# Файлы метаинформации CMake
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
*.cmake
|
||||
*.ninja
|
||||
.ninja_deps
|
||||
.ninja_log
|
||||
|
||||
# QML cache
|
||||
*.qmlc
|
||||
*.jsc
|
||||
|
||||
# Скрытые файлы и папки
|
||||
.*
|
||||
!/.gitignore
|
||||
!/.gitattributes
|
||||
|
||||
# Временные файлы
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# MacOS файлы
|
||||
.DS_Store
|
||||
._*
|
||||
|
||||
# Папки зависимостей (если используются)
|
||||
3rdparty/
|
||||
libs/
|
||||
@@ -0,0 +1,92 @@
|
||||
QT += core gui websockets network sql multimedia
|
||||
|
||||
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
||||
|
||||
CONFIG += c++11 debug
|
||||
CONFIG -= app_bundle
|
||||
|
||||
# Для MinGW
|
||||
win32: {
|
||||
LIBS += -lole32 -loleaut32 -luuid -lstrmiids -lquartz -ldsound
|
||||
LIBS += -lwinmm
|
||||
}
|
||||
|
||||
# Для MSVC (если используете Visual Studio)
|
||||
# win32:msvc {
|
||||
# LIBS += strmiids.lib quartz.lib ole32.lib oleaut32.lib uuid.lib
|
||||
# }
|
||||
|
||||
RC_ICONS = ico\app_icon.ico
|
||||
|
||||
INCLUDEPATH += $$PWD
|
||||
|
||||
SOURCES += \
|
||||
emoteprovider.cpp \
|
||||
fcolorsetting.cpp \
|
||||
fcreatechat.cpp \
|
||||
fcreatenotify.cpp \
|
||||
ffontsetting.cpp \
|
||||
fsettingsws.cpp \
|
||||
fsinglegrid.cpp \
|
||||
main.cpp \
|
||||
neuralnetworkmanager.cpp \
|
||||
soundmanager.cpp \
|
||||
tauth.cpp \
|
||||
ttw_api.cpp \
|
||||
twitchmessage.cpp \
|
||||
udatabase.cpp \
|
||||
ugeneral.cpp \
|
||||
ulink.cpp \
|
||||
user.cpp \
|
||||
user_manager.cpp \
|
||||
userwidget.cpp \
|
||||
webserverchat.cpp \
|
||||
webservernotify.cpp \
|
||||
websocketclient.cpp
|
||||
|
||||
HEADERS += \
|
||||
badge.h \
|
||||
emoteprovider.h \
|
||||
fcolorsetting.h \
|
||||
fcreatechat.h \
|
||||
fcreatenotify.h \
|
||||
ffontsetting.h \
|
||||
fsettingsws.h \
|
||||
fsinglegrid.h \
|
||||
miniaudio.h \
|
||||
neuralnetworkmanager.h \
|
||||
soundmanager.h \
|
||||
tauth.h \
|
||||
timerinfo.h \
|
||||
ttw_api.h \
|
||||
ttw_types.h \
|
||||
twitchmessage.h \
|
||||
udatabase.h \
|
||||
ugeneral.h \
|
||||
ulink.h \
|
||||
user.h \
|
||||
user_manager.h \
|
||||
userwidget.h \
|
||||
webserverchat.h \
|
||||
webservernotify.h \
|
||||
websocketclient.h
|
||||
|
||||
FORMS += \
|
||||
fcolorsetting.ui \
|
||||
fcreatechat.ui \
|
||||
fcreatenotify.ui \
|
||||
ffontsetting.ui \
|
||||
fsettingsws.ui \
|
||||
fsinglegrid.ui \
|
||||
ugeneral.ui \
|
||||
ulink.ui
|
||||
|
||||
TRANSLATIONS += \
|
||||
TTW_Bot_ru_RU.ts
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
DISTFILES +=
|
||||
@@ -0,0 +1,37 @@
|
||||
#include <windows.h>
|
||||
|
||||
IDI_ICON1 ICON DISCARDABLE "C:/Users/PTyTb/Documents/cpp/TTW_Bot/ico/app_icon.ico"
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 0,0,0,0
|
||||
PRODUCTVERSION 0,0,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS__WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "\0"
|
||||
VALUE "FileDescription", "\0"
|
||||
VALUE "FileVersion", "0.0.0.0\0"
|
||||
VALUE "LegalCopyright", "\0"
|
||||
VALUE "OriginalFilename", "TTW_Bot.exe\0"
|
||||
VALUE "ProductName", "TTW_Bot\0"
|
||||
VALUE "ProductVersion", "0.0.0.0\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 1200
|
||||
END
|
||||
END
|
||||
/* End of Version info */
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="TTW_Bot_app_ru_RU"></TS>
|
||||
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,21 @@
|
||||
#ifndef BADGE_H
|
||||
#define BADGE_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
struct BadgeVersion {
|
||||
QString id;
|
||||
QString imageUrl1x;
|
||||
QString imageUrl2x;
|
||||
QString imageUrl4x;
|
||||
QString title;
|
||||
QString description;
|
||||
};
|
||||
|
||||
struct ChatBadge {
|
||||
QString setId;
|
||||
QVector<BadgeVersion> versions;
|
||||
};
|
||||
|
||||
#endif // BADGE_H
|
||||
@@ -0,0 +1,418 @@
|
||||
// emoteprovider.cpp
|
||||
#include "emoteprovider.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
// EmoteProvider implementation
|
||||
EmoteProvider::EmoteProvider(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this)) {
|
||||
}
|
||||
|
||||
EmoteProvider::~EmoteProvider() {
|
||||
// QNetworkAccessManager будет удален автоматически как дочерний объект
|
||||
}
|
||||
|
||||
void EmoteProvider::setLogCallback(LogCallback callback) {
|
||||
m_logCallback = callback;
|
||||
}
|
||||
|
||||
void EmoteProvider::log(const QString &method, const QString &message, LogLevel level) {
|
||||
if (m_logCallback) {
|
||||
m_logCallback(metaObject()->className(), method, message, level);
|
||||
}
|
||||
}
|
||||
|
||||
// BTTVProvider implementation
|
||||
BTTVProvider::BTTVProvider(QObject *parent)
|
||||
: EmoteProvider(parent) {
|
||||
}
|
||||
|
||||
BTTVProvider::~BTTVProvider() {
|
||||
m_emotes.clear();
|
||||
}
|
||||
|
||||
void BTTVProvider::fetchGlobal() {
|
||||
log("fetchGlobal", "Fetching global BTTV emotes", LOG_INFO);
|
||||
|
||||
QNetworkRequest request(QUrl(getBaseUrl() + "emotes/global"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader,
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished,
|
||||
this, [this, reply]() { onGlobalReplyFinished(reply); });
|
||||
}
|
||||
|
||||
void BTTVProvider::fetchCustom(const QString &userId) {
|
||||
if (userId.isEmpty()) {
|
||||
log("fetchCustom", "User ID is empty", LOG_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
log("fetchCustom", QString("Fetching custom BTTV emotes for user: %1").arg(userId), LOG_INFO);
|
||||
m_lastUserId = userId;
|
||||
|
||||
QNetworkRequest request(QUrl(getBaseUrl() + "users/twitch/" + userId));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader,
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished,
|
||||
this, [this, reply]() { onCustomReplyFinished(reply); });
|
||||
}
|
||||
|
||||
QString BTTVProvider::getEmoteUrl(const QString &emoteName) {
|
||||
for (const auto &emote : m_emotes) {
|
||||
if (emote.code == emoteName) {
|
||||
return emote.url();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void BTTVProvider::onGlobalReplyFinished(QNetworkReply *reply) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
log("onGlobalReplyFinished",
|
||||
QString("Network error: %1").arg(reply->errorString()),
|
||||
LOG_ERROR);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
parseGlobalResponse(data);
|
||||
reply->deleteLater();
|
||||
|
||||
emit emotesLoaded();
|
||||
}
|
||||
|
||||
void BTTVProvider::onCustomReplyFinished(QNetworkReply *reply) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
log("onCustomReplyFinished",
|
||||
QString("Network error: %1").arg(reply->errorString()),
|
||||
LOG_ERROR);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
parseCustomResponse(data, m_lastUserId);
|
||||
reply->deleteLater();
|
||||
|
||||
emit emotesLoaded();
|
||||
}
|
||||
|
||||
void BTTVProvider::parseGlobalResponse(const QByteArray &data) {
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
log("parseGlobalResponse",
|
||||
QString("JSON parse error: %1").arg(parseError.errorString()),
|
||||
LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.isArray()) {
|
||||
log("parseGlobalResponse", "Expected JSON array", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array = doc.array();
|
||||
m_emotes.clear();
|
||||
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject()) continue;
|
||||
|
||||
QJsonObject obj = value.toObject();
|
||||
BTTVEmote emote;
|
||||
emote.id = obj.value("id").toString();
|
||||
emote.code = obj.value("code").toString();
|
||||
|
||||
if (!emote.id.isEmpty() && !emote.code.isEmpty()) {
|
||||
m_emotes.append(emote);
|
||||
}
|
||||
}
|
||||
|
||||
log("parseGlobalResponse",
|
||||
QString("Loaded %1 global emotes").arg(m_emotes.size()),
|
||||
LOG_INFO);
|
||||
}
|
||||
|
||||
void BTTVProvider::parseCustomResponse(const QByteArray &data, const QString &userId) {
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
log("parseCustomResponse",
|
||||
QString("JSON parse error: %1").arg(parseError.errorString()),
|
||||
LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.isObject()) {
|
||||
log("parseCustomResponse", "Expected JSON object", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
|
||||
auto parseEmotesArray = [this](const QJsonArray &array) {
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject()) continue;
|
||||
|
||||
QJsonObject obj = value.toObject();
|
||||
BTTVEmote emote;
|
||||
emote.id = obj.value("id").toString();
|
||||
emote.code = obj.value("code").toString();
|
||||
|
||||
if (!emote.id.isEmpty() && !emote.code.isEmpty()) {
|
||||
m_emotes.append(emote);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Очищаем только пользовательские эмодзи (можно оптимизировать)
|
||||
// В реальном приложении нужно хранить отдельно глобальные и пользовательские
|
||||
|
||||
// Парсим channelEmotes
|
||||
if (root.contains("channelEmotes") && root["channelEmotes"].isArray()) {
|
||||
parseEmotesArray(root["channelEmotes"].toArray());
|
||||
}
|
||||
|
||||
// Парсим sharedEmotes
|
||||
if (root.contains("sharedEmotes") && root["sharedEmotes"].isArray()) {
|
||||
parseEmotesArray(root["sharedEmotes"].toArray());
|
||||
}
|
||||
|
||||
log("parseCustomResponse",
|
||||
QString("Loaded %1 custom emotes for user %2").arg(m_emotes.size()).arg(userId),
|
||||
LOG_INFO);
|
||||
}
|
||||
|
||||
// SevenTVProvider implementation
|
||||
SevenTVProvider::SevenTVProvider(QObject *parent)
|
||||
: EmoteProvider(parent) {
|
||||
}
|
||||
|
||||
SevenTVProvider::~SevenTVProvider() {
|
||||
m_emotes.clear();
|
||||
}
|
||||
|
||||
void SevenTVProvider::fetchGlobal() {
|
||||
log("fetchGlobal", "Fetching global 7TV emotes", LOG_INFO);
|
||||
|
||||
QNetworkRequest request(QUrl(getBaseUrl() + "emote-sets/global"));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader,
|
||||
"Mozilla/5.0");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished,
|
||||
this, [this, reply]() { onGlobalReplyFinished(reply); });
|
||||
}
|
||||
|
||||
void SevenTVProvider::fetchCustom(const QString &userId) {
|
||||
if (userId.isEmpty()) {
|
||||
log("fetchCustom", "User ID is empty", LOG_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
log("fetchCustom", QString("Fetching custom 7TV emotes for user: %1").arg(userId), LOG_INFO);
|
||||
m_lastUserId = userId;
|
||||
|
||||
QNetworkRequest request(QUrl(getBaseUrl() + "users/twitch/" + userId));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader,
|
||||
"Mozilla/5.0");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished,
|
||||
this, [this, reply]() { onCustomReplyFinished(reply); });
|
||||
}
|
||||
|
||||
QString SevenTVProvider::getEmoteUrl(const QString &emoteName) {
|
||||
for (const auto &emote : m_emotes) {
|
||||
if (emote.code == emoteName) {
|
||||
return emote.url;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void SevenTVProvider::onGlobalReplyFinished(QNetworkReply *reply) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
log("onGlobalReplyFinished",
|
||||
QString("Network error: %1").arg(reply->errorString()),
|
||||
LOG_ERROR);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
parseGlobalResponse(data);
|
||||
reply->deleteLater();
|
||||
|
||||
emit emotesLoaded();
|
||||
}
|
||||
|
||||
void SevenTVProvider::onCustomReplyFinished(QNetworkReply *reply) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
log("onCustomReplyFinished",
|
||||
QString("Network error: %1").arg(reply->errorString()),
|
||||
LOG_ERROR);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
parseCustomResponse(data, m_lastUserId);
|
||||
reply->deleteLater();
|
||||
|
||||
emit emotesLoaded();
|
||||
}
|
||||
|
||||
void SevenTVProvider::parseGlobalResponse(const QByteArray &data) {
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
log("parseGlobalResponse",
|
||||
QString("JSON parse error: %1").arg(parseError.errorString()),
|
||||
LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.isObject()) {
|
||||
log("parseGlobalResponse", "Expected JSON object", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
m_emotes.clear();
|
||||
|
||||
if (!root.contains("emotes") || !root["emotes"].isArray()) {
|
||||
log("parseGlobalResponse", "No emotes array found", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray emotesArray = root["emotes"].toArray();
|
||||
|
||||
for (const QJsonValue &emoteValue : emotesArray) {
|
||||
if (!emoteValue.isObject()) continue;
|
||||
|
||||
QJsonObject emoteObj = emoteValue.toObject();
|
||||
SevenTVEmote emote;
|
||||
|
||||
emote.id = emoteObj.value("id").toString();
|
||||
emote.code = emoteObj.value("name").toString();
|
||||
|
||||
// Парсим URL
|
||||
if (emoteObj.contains("data") && emoteObj["data"].isObject()) {
|
||||
QJsonObject dataObj = emoteObj["data"].toObject();
|
||||
|
||||
if (dataObj.contains("host") && dataObj["host"].isObject()) {
|
||||
QJsonObject hostObj = dataObj["host"].toObject();
|
||||
|
||||
if (hostObj.contains("url")) {
|
||||
QString baseUrl = "https:" + hostObj["url"].toString();
|
||||
|
||||
if (hostObj.contains("files") && hostObj["files"].isArray()) {
|
||||
QJsonArray filesArray = hostObj["files"].toArray();
|
||||
|
||||
if (!filesArray.isEmpty() && filesArray[0].isObject()) {
|
||||
QJsonObject fileObj = filesArray[0].toObject();
|
||||
if (fileObj.contains("name")) {
|
||||
emote.url = baseUrl + "/" + fileObj["name"].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!emote.id.isEmpty() && !emote.code.isEmpty() && !emote.url.isEmpty()) {
|
||||
m_emotes.append(emote);
|
||||
}
|
||||
}
|
||||
|
||||
log("parseGlobalResponse",
|
||||
QString("Loaded %1 global emotes").arg(m_emotes.size()),
|
||||
LOG_INFO);
|
||||
}
|
||||
|
||||
void SevenTVProvider::parseCustomResponse(const QByteArray &data, const QString &userId) {
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
log("parseCustomResponse",
|
||||
QString("JSON parse error: %1").arg(parseError.errorString()),
|
||||
LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.isObject()) {
|
||||
log("parseCustomResponse", "Expected JSON object", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
|
||||
if (!root.contains("emote_set") || !root["emote_set"].isObject()) {
|
||||
log("parseCustomResponse", "No emote_set found", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject emoteSet = root["emote_set"].toObject();
|
||||
|
||||
if (!emoteSet.contains("emotes") || !emoteSet["emotes"].isArray()) {
|
||||
log("parseCustomResponse", "No emotes array in emote_set", LOG_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray emotesArray = emoteSet["emotes"].toArray();
|
||||
|
||||
for (const QJsonValue &emoteValue : emotesArray) {
|
||||
if (!emoteValue.isObject()) continue;
|
||||
|
||||
QJsonObject emoteObj = emoteValue.toObject();
|
||||
SevenTVEmote emote;
|
||||
|
||||
emote.id = emoteObj.value("id").toString();
|
||||
emote.code = emoteObj.value("name").toString();
|
||||
|
||||
// Парсим URL (аналогично parseGlobalResponse)
|
||||
if (emoteObj.contains("data") && emoteObj["data"].isObject()) {
|
||||
QJsonObject dataObj = emoteObj["data"].toObject();
|
||||
|
||||
if (dataObj.contains("host") && dataObj["host"].isObject()) {
|
||||
QJsonObject hostObj = dataObj["host"].toObject();
|
||||
|
||||
if (hostObj.contains("url")) {
|
||||
QString baseUrl = "https:" + hostObj["url"].toString();
|
||||
|
||||
if (hostObj.contains("files") && hostObj["files"].isArray()) {
|
||||
QJsonArray filesArray = hostObj["files"].toArray();
|
||||
|
||||
if (!filesArray.isEmpty() && filesArray[0].isObject()) {
|
||||
QJsonObject fileObj = filesArray[0].toObject();
|
||||
if (fileObj.contains("name")) {
|
||||
emote.url = baseUrl + "/" + fileObj["name"].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!emote.id.isEmpty() && !emote.code.isEmpty() && !emote.url.isEmpty()) {
|
||||
m_emotes.append(emote);
|
||||
}
|
||||
}
|
||||
|
||||
log("parseCustomResponse",
|
||||
QString("Loaded %1 custom emotes for user %2").arg(m_emotes.size()).arg(userId),
|
||||
LOG_INFO);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// emoteprovider.h
|
||||
#ifndef EMOTEPROVIDER_H
|
||||
#define EMOTEPROVIDER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <functional>
|
||||
|
||||
enum LogLevel {
|
||||
LOG_INFO = 0,
|
||||
LOG_WARNING = 1,
|
||||
LOG_ERROR = 2
|
||||
};
|
||||
|
||||
using LogCallback = std::function<void(const QString& module,
|
||||
const QString& method,
|
||||
const QString& message,
|
||||
LogLevel level)>;
|
||||
|
||||
struct BTTVEmote {
|
||||
QString id;
|
||||
QString code;
|
||||
QString url() const {
|
||||
return QString("https://cdn.betterttv.net/emote/%1/1x").arg(id);
|
||||
}
|
||||
};
|
||||
|
||||
struct SevenTVEmote {
|
||||
QString id;
|
||||
QString code;
|
||||
QString url;
|
||||
};
|
||||
|
||||
// Абстрактный базовый класс для провайдеров эмодзи
|
||||
class EmoteProvider : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EmoteProvider(QObject *parent = nullptr);
|
||||
virtual ~EmoteProvider();
|
||||
|
||||
void setLogCallback(LogCallback callback);
|
||||
virtual void fetchGlobal() = 0;
|
||||
virtual void fetchCustom(const QString &userId) = 0;
|
||||
virtual QString getEmoteUrl(const QString &emoteName) = 0;
|
||||
|
||||
signals:
|
||||
void emotesLoaded();
|
||||
|
||||
protected:
|
||||
void log(const QString &method, const QString &message, LogLevel level);
|
||||
virtual QString getBaseUrl() const = 0;
|
||||
virtual void parseGlobalResponse(const QByteArray &data) = 0;
|
||||
virtual void parseCustomResponse(const QByteArray &data, const QString &userId) = 0;
|
||||
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
LogCallback m_logCallback;
|
||||
};
|
||||
|
||||
// Провайдер для BetterTTV
|
||||
class BTTVProvider : public EmoteProvider {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BTTVProvider(QObject *parent = nullptr);
|
||||
~BTTVProvider();
|
||||
|
||||
void fetchGlobal() override;
|
||||
void fetchCustom(const QString &userId) override;
|
||||
QString getEmoteUrl(const QString &emoteName) override;
|
||||
|
||||
private slots:
|
||||
void onGlobalReplyFinished(QNetworkReply *reply);
|
||||
void onCustomReplyFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QString getBaseUrl() const override { return "https://api.betterttv.net/3/cached/"; }
|
||||
void parseGlobalResponse(const QByteArray &data) override;
|
||||
void parseCustomResponse(const QByteArray &data, const QString &userId) override;
|
||||
|
||||
QList<BTTVEmote> m_emotes;
|
||||
QString m_lastUserId;
|
||||
};
|
||||
|
||||
// Провайдер для 7TV
|
||||
class SevenTVProvider : public EmoteProvider {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SevenTVProvider(QObject *parent = nullptr);
|
||||
~SevenTVProvider();
|
||||
|
||||
void fetchGlobal() override;
|
||||
void fetchCustom(const QString &userId) override;
|
||||
QString getEmoteUrl(const QString &emoteName) override;
|
||||
|
||||
private slots:
|
||||
void onGlobalReplyFinished(QNetworkReply *reply);
|
||||
void onCustomReplyFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QString getBaseUrl() const override { return "https://api.7tv.app/v3/"; }
|
||||
void parseGlobalResponse(const QByteArray &data) override;
|
||||
void parseCustomResponse(const QByteArray &data, const QString &userId) override;
|
||||
|
||||
QList<SevenTVEmote> m_emotes;
|
||||
QString m_lastUserId;
|
||||
};
|
||||
|
||||
#endif // EMOTEPROVIDER_H
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "fcolorsetting.h"
|
||||
#include "ui_fcolorsetting.h"
|
||||
|
||||
FColorSetting::FColorSetting(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, cbBlockColor(nullptr)
|
||||
, cbBorderColor(nullptr)
|
||||
, cbBackgroundColor(nullptr)
|
||||
, sbBorderSize(nullptr)
|
||||
, sbPadding(nullptr)
|
||||
, hsBlockTransparant(nullptr)
|
||||
, ui(new Ui::FColorSetting)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setupColorComboBox(ui->cbBackgroundColor);
|
||||
setupColorComboBox(ui->cbBlockColor);
|
||||
setupColorComboBox(ui->cbBorderColor);
|
||||
cbBlockColor = ui->cbBlockColor;
|
||||
cbBorderColor = ui->cbBorderColor;
|
||||
cbBackgroundColor = ui->cbBackgroundColor;
|
||||
sbBorderSize = ui->sbBorderSize;
|
||||
sbPadding = ui->sbPadding;
|
||||
hsBlockTransparant = ui->hsBlockTransparant;
|
||||
|
||||
setDefaultColor(ui->cbBackgroundColor, "lime");
|
||||
}
|
||||
|
||||
FColorSetting::~FColorSetting()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
|
||||
void FColorSetting::setupColorComboBox(QComboBox* comboBox)
|
||||
{
|
||||
comboBox->clear();
|
||||
|
||||
// Добавляем цвета
|
||||
QStringList colorNames = QColor::colorNames();
|
||||
foreach (const QString &name, colorNames) {
|
||||
QColor color(name);
|
||||
QPixmap pixmap(20, 20);
|
||||
pixmap.fill(color);
|
||||
comboBox->addItem(QIcon(pixmap), name, color);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
QString FColorSetting::hexToColorName(const QString &hexColor)
|
||||
{
|
||||
// Создаем QColor из HEX
|
||||
QColor color(hexColor);
|
||||
if (!color.isValid()) {
|
||||
return hexColor; // Возвращаем HEX, если цвет невалиден
|
||||
}
|
||||
|
||||
// Получаем все названия цветов из Qt
|
||||
QStringList colorNames = QColor::colorNames();
|
||||
|
||||
// Ищем ближайшее название по RGB
|
||||
QString closestName;
|
||||
int minDistance = INT_MAX;
|
||||
|
||||
for (const QString &name : colorNames) {
|
||||
QColor namedColor(name);
|
||||
if (!namedColor.isValid()) continue;
|
||||
|
||||
// Вычисляем евклидово расстояние в RGB пространстве
|
||||
int distance =
|
||||
qAbs(color.red() - namedColor.red()) +
|
||||
qAbs(color.green() - namedColor.green()) +
|
||||
qAbs(color.blue() - namedColor.blue());
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestName = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Если нашли достаточно близкий цвет (разница небольшая)
|
||||
if (minDistance < 50 && !closestName.isEmpty()) {
|
||||
return closestName;
|
||||
}
|
||||
|
||||
// Если точного соответствия нет, пытаемся найти по HEX в таблице соответствий
|
||||
static QMap<QString, QString> hexToName = {
|
||||
{"#f0f8ff", "aliceblue"},
|
||||
{"#faebd7", "antiquewhite"},
|
||||
{"#00ffff", "aqua"},
|
||||
{"#7fffd4", "aquamarine"},
|
||||
{"#f0ffff", "azure"},
|
||||
{"#f5f5dc", "beige"},
|
||||
{"#ffe4c4", "bisque"},
|
||||
{"#000000", "black"},
|
||||
{"#ffebcd", "blanchedalmond"},
|
||||
{"#0000ff", "blue"},
|
||||
{"#8a2be2", "blueviolet"},
|
||||
{"#a52a2a", "brown"},
|
||||
{"#deb887", "burlywood"},
|
||||
{"#5f9ea0", "cadetblue"},
|
||||
{"#7fff00", "chartreuse"},
|
||||
{"#d2691e", "chocolate"},
|
||||
{"#ff7f50", "coral"},
|
||||
{"#6495ed", "cornflowerblue"},
|
||||
{"#fff8dc", "cornsilk"},
|
||||
{"#dc143c", "crimson"},
|
||||
{"#00ffff", "cyan"},
|
||||
{"#00008b", "darkblue"},
|
||||
{"#008b8b", "darkcyan"},
|
||||
{"#b8860b", "darkgoldenrod"},
|
||||
{"#a9a9a9", "darkgray"},
|
||||
{"#006400", "darkgreen"},
|
||||
{"#a9a9a9", "darkgrey"},
|
||||
{"#bdb76b", "darkkhaki"},
|
||||
{"#8b008b", "darkmagenta"},
|
||||
{"#556b2f", "darkolivegreen"},
|
||||
{"#ff8c00", "darkorange"},
|
||||
{"#9932cc", "darkorchid"},
|
||||
{"#8b0000", "darkred"},
|
||||
{"#e9967a", "darksalmon"},
|
||||
{"#8fbc8f", "darkseagreen"},
|
||||
{"#483d8b", "darkslateblue"},
|
||||
{"#2f4f4f", "darkslategray"},
|
||||
{"#2f4f4f", "darkslategrey"},
|
||||
{"#00ced1", "darkturquoise"},
|
||||
{"#9400d3", "darkviolet"},
|
||||
{"#ff1493", "deeppink"},
|
||||
{"#00bfff", "deepskyblue"},
|
||||
{"#696969", "dimgray"},
|
||||
{"#696969", "dimgrey"},
|
||||
{"#1e90ff", "dodgerblue"},
|
||||
{"#b22222", "firebrick"},
|
||||
{"#fffaf0", "floralwhite"},
|
||||
{"#228b22", "forestgreen"},
|
||||
{"#dcdcdc", "gainsboro"},
|
||||
{"#f8f8ff", "ghostwhite"},
|
||||
{"#ffd700", "gold"},
|
||||
{"#daa520", "goldenrod"},
|
||||
{"#808080", "gray"},
|
||||
{"#008000", "green"},
|
||||
{"#adff2f", "greenyellow"},
|
||||
{"#808080", "grey"},
|
||||
{"#f0fff0", "honeydew"},
|
||||
{"#ff69b4", "hotpink"},
|
||||
{"#cd5c5c", "indianred"},
|
||||
{"#4b0082", "indigo"},
|
||||
{"#fffff0", "ivory"},
|
||||
{"#f0e68c", "khaki"},
|
||||
{"#e6e6fa", "lavender"},
|
||||
{"#fff0f5", "lavenderblush"},
|
||||
{"#7cfc00", "lawngreen"},
|
||||
{"#fffacd", "lemonchiffon"},
|
||||
{"#add8e6", "lightblue"},
|
||||
{"#f08080", "lightcoral"},
|
||||
{"#e0ffff", "lightcyan"},
|
||||
{"#fafad2", "lightgoldenrodyellow"},
|
||||
{"#d3d3d3", "lightgray"},
|
||||
{"#90ee90", "lightgreen"},
|
||||
{"#d3d3d3", "lightgrey"},
|
||||
{"#ffb6c1", "lightpink"},
|
||||
{"#ffa07a", "lightsalmon"},
|
||||
{"#20b2aa", "lightseagreen"},
|
||||
{"#87cefa", "lightskyblue"},
|
||||
{"#778899", "lightslategray"},
|
||||
{"#778899", "lightslategrey"},
|
||||
{"#b0c4de", "lightsteelblue"},
|
||||
{"#ffffe0", "lightyellow"},
|
||||
{"#00ff00", "lime"},
|
||||
{"#32cd32", "limegreen"},
|
||||
{"#faf0e6", "linen"},
|
||||
{"#800000", "maroon"},
|
||||
{"#66cdaa", "mediumaquamarine"},
|
||||
{"#0000cd", "mediumblue"},
|
||||
{"#ba55d3", "mediumorchid"},
|
||||
{"#9370db", "mediumpurple"},
|
||||
{"#3cb371", "mediumseagreen"},
|
||||
{"#7b68ee", "mediumslateblue"},
|
||||
{"#00fa9a", "mediumspringgreen"},
|
||||
{"#48d1cc", "mediumturquoise"},
|
||||
{"#c71585", "mediumvioletred"},
|
||||
{"#191970", "midnightblue"},
|
||||
{"#f5fffa", "mintcream"},
|
||||
{"#ffe4e1", "mistyrose"},
|
||||
{"#ffe4b5", "moccasin"},
|
||||
{"#ffdead", "navajowhite"},
|
||||
{"#000080", "navy"},
|
||||
{"#fdf5e6", "oldlace"},
|
||||
{"#808000", "olive"},
|
||||
{"#6b8e23", "olivedrab"},
|
||||
{"#ffa500", "orange"},
|
||||
{"#ff4500", "orangered"},
|
||||
{"#da70d6", "orchid"},
|
||||
{"#eee8aa", "palegoldenrod"},
|
||||
{"#98fb98", "palegreen"},
|
||||
{"#afeeee", "paleturquoise"},
|
||||
{"#db7093", "palevioletred"},
|
||||
{"#ffefd5", "papayawhip"},
|
||||
{"#ffdab9", "peachpuff"},
|
||||
{"#cd853f", "peru"},
|
||||
{"#ffc0cb", "pink"},
|
||||
{"#dda0dd", "plum"},
|
||||
{"#b0e0e6", "powderblue"},
|
||||
{"#800080", "purple"},
|
||||
{"#663399", "rebeccapurple"},
|
||||
{"#ff0000", "red"},
|
||||
{"#bc8f8f", "rosybrown"},
|
||||
{"#4169e1", "royalblue"},
|
||||
{"#8b4513", "saddlebrown"},
|
||||
{"#fa8072", "salmon"},
|
||||
{"#f4a460", "sandybrown"},
|
||||
{"#2e8b57", "seagreen"},
|
||||
{"#fff5ee", "seashell"},
|
||||
{"#a0522d", "sienna"},
|
||||
{"#c0c0c0", "silver"},
|
||||
{"#87ceeb", "skyblue"},
|
||||
{"#6a5acd", "slateblue"},
|
||||
{"#708090", "slategray"},
|
||||
{"#708090", "slategrey"},
|
||||
{"#fffafa", "snow"},
|
||||
{"#00ff7f", "springgreen"},
|
||||
{"#4682b4", "steelblue"},
|
||||
{"#d2b48c", "tan"},
|
||||
{"#008080", "teal"},
|
||||
{"#d8bfd8", "thistle"},
|
||||
{"#ff6347", "tomato"},
|
||||
{"#40e0d0", "turquoise"},
|
||||
{"#ee82ee", "violet"},
|
||||
{"#f5deb3", "wheat"},
|
||||
{"#ffffff", "white"},
|
||||
{"#f5f5f5", "whitesmoke"},
|
||||
{"#ffff00", "yellow"},
|
||||
{"#9acd32", "yellowgreen"}
|
||||
};
|
||||
|
||||
QString lowerHex = hexColor.toLower();
|
||||
if (hexToName.contains(lowerHex)) {
|
||||
return hexToName[lowerHex];
|
||||
}
|
||||
|
||||
// Если ничего не нашли, возвращаем HEX
|
||||
return hexColor;
|
||||
}
|
||||
|
||||
void FColorSetting::setDefaultColor(QComboBox* comboBox, const QString& colorName)
|
||||
{
|
||||
// Ищем индекс цвета в комбобоксе
|
||||
int index = comboBox->findText(colorName, Qt::MatchFixedString);
|
||||
if (index != -1) {
|
||||
comboBox->setCurrentIndex(index);
|
||||
} else {
|
||||
comboBox->setCurrentIndex(0); // Устанавливаем первый цвет как запасной вариант
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef FCOLORSETTING_H
|
||||
#define FCOLORSETTING_H
|
||||
|
||||
#include "qspinbox.h"
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
|
||||
namespace Ui {
|
||||
class FColorSetting;
|
||||
}
|
||||
|
||||
class FColorSetting : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FColorSetting(QWidget *parent = nullptr);
|
||||
~FColorSetting();
|
||||
QComboBox *cbBlockColor;
|
||||
QComboBox *cbBorderColor;
|
||||
QComboBox *cbBackgroundColor;
|
||||
QSpinBox *sbBorderSize;
|
||||
QSpinBox *sbPadding;
|
||||
QSlider *hsBlockTransparant;
|
||||
QString hexToColorName(const QString &hexColor);
|
||||
private:
|
||||
Ui::FColorSetting *ui;
|
||||
|
||||
void setupColorComboBox(QComboBox* comboBox);
|
||||
void setDefaultColor(QComboBox* comboBox, const QString& colorName);
|
||||
};
|
||||
|
||||
#endif // FCOLORSETTING_H
|
||||
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FColorSetting</class>
|
||||
<widget class="QWidget" name="FColorSetting">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>333</width>
|
||||
<height>104</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="gridLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>331</width>
|
||||
<height>101</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Цвет рамки</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="cbBorderColor"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Цвет фона</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Цвет блока</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QComboBox" name="cbBackgroundColor"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QComboBox" name="cbBlockColor"/>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Отступы</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QSpinBox" name="sbPadding">
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="sbBorderSize">
|
||||
<property name="maximum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Толщина рамки</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Прозрачность блока</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QSlider" name="hsBlockTransparant">
|
||||
<property name="maximum">
|
||||
<number>255</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,344 @@
|
||||
#include "fcreatechat.h"
|
||||
#include "ui_fcreatechat.h"
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
|
||||
FCreateChat::FCreateChat(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_chatServer(nullptr)
|
||||
, m_StyleChat(nullptr)
|
||||
, ui(new Ui::FCreateChat)
|
||||
, m_isEditMode(false)
|
||||
, m_existingServerName("")
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowTitle("TTW Bot app: Создать чат");
|
||||
m_StyleChat = nullptr;
|
||||
|
||||
QStringList fontList;
|
||||
|
||||
|
||||
// Создаем сервер чата на порту 7998 (можно сделать настраиваемым)
|
||||
m_chatServer = nullptr;
|
||||
|
||||
// Подключаем кнопки
|
||||
connect(ui->btnTest, &QPushButton::clicked, this, &FCreateChat::onBtnTestClicked);
|
||||
connect(ui->btnAdd, &QPushButton::clicked, this, &FCreateChat::onBtnAddClicked);
|
||||
|
||||
}
|
||||
|
||||
FCreateChat::~FCreateChat()
|
||||
{
|
||||
if (m_StyleChat) {
|
||||
delete m_StyleChat;
|
||||
}
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void FCreateChat::setEditMode(bool isEditMode)
|
||||
{
|
||||
m_isEditMode = isEditMode;
|
||||
if (isEditMode) {
|
||||
ui->btnAdd->setText("Изменить");
|
||||
setWindowTitle("TTW Bot app: Редактировать чат");
|
||||
} else {
|
||||
ui->btnAdd->setText("Создать");
|
||||
setWindowTitle("TTW Bot app: Создать чат");
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateChat::loadExistingServer(HttpServerChat *server, const QString &name)
|
||||
{
|
||||
if (!server) return;
|
||||
|
||||
m_isEditMode = true;
|
||||
m_chatServer = server;
|
||||
m_existingServerName = name;
|
||||
|
||||
setEditMode(true);
|
||||
|
||||
FSettingsWS *settingsWS = ui->widget;
|
||||
if (settingsWS) {
|
||||
settingsWS->sbPort->setValue(server->port());
|
||||
settingsWS->cbFreez->setChecked(server->isFreez());
|
||||
settingsWS->sbTime->setValue(server->getMessageTimeout());
|
||||
settingsWS->sbCount->setValue(server->getMaxMsgCount());
|
||||
}
|
||||
|
||||
ui->lineEdit->setText(name);
|
||||
|
||||
FColorSetting *colorSetting = ui->wBlock;
|
||||
if (colorSetting) {
|
||||
QString backgroundColor = colorSetting->hexToColorName(server->getBackgroundColor());
|
||||
colorSetting->cbBackgroundColor->setCurrentText(backgroundColor);
|
||||
|
||||
|
||||
QString blockColor = colorSetting->hexToColorName(server->getBlockColor());
|
||||
colorSetting->cbBlockColor->setCurrentText(blockColor);
|
||||
|
||||
|
||||
QString borderColor = colorSetting->hexToColorName(server->getBorderColor());
|
||||
colorSetting->cbBorderColor->setCurrentText(borderColor);
|
||||
|
||||
|
||||
colorSetting->sbBorderSize->setValue(server->getBorderSize());
|
||||
colorSetting->sbPadding->setValue(server->getPadding());
|
||||
colorSetting->hsBlockTransparant->setValue(server->getTransparency());
|
||||
}
|
||||
|
||||
FFontSetting *fontSetting = ui->wFont;
|
||||
if (fontSetting) {
|
||||
QString fontFamily = colorSetting->hexToColorName(server->getFontFamily());
|
||||
fontSetting->cbFontStyle->setCurrentText(fontFamily);
|
||||
|
||||
fontSetting->sbFontSize->setValue(server->getFontSize());
|
||||
|
||||
QString fontColor = colorSetting->hexToColorName(server->getFontColor());
|
||||
fontSetting->cbFontColor->setCurrentText(fontColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FCreateChat::createServer()
|
||||
{
|
||||
if (m_chatServer) {
|
||||
m_chatServer->stop();
|
||||
delete m_chatServer;
|
||||
m_chatServer = nullptr;
|
||||
}
|
||||
|
||||
// Получаем настройки из формы
|
||||
FSettingsWS *settingsWS = ui->widget;
|
||||
if (!settingsWS) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не найдены настройки сервера");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем порт из настроек
|
||||
int port = settingsWS->sbPort->value();
|
||||
|
||||
// Получаем список шрифтов из FFontSetting
|
||||
FFontSetting *fontSetting = ui->wFont;
|
||||
QStringList fontList;
|
||||
|
||||
if (fontSetting && fontSetting->cbFontStyle) {
|
||||
// Добавляем все шрифты из комбобокса
|
||||
for (int i = 0; i < fontSetting->cbFontStyle->count(); ++i) {
|
||||
fontList.append(fontSetting->cbFontStyle->itemText(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем цвет фона из FColorSetting
|
||||
FColorSetting *colorSetting = ui->wBlock;
|
||||
QString backgroundColor = "#89F336"; // По умолчанию черный
|
||||
|
||||
if (colorSetting && colorSetting->cbBackgroundColor) {
|
||||
QColor bgColor = colorSetting->cbBackgroundColor->currentData().value<QColor>();
|
||||
if (!bgColor.isValid()) {
|
||||
bgColor = QColor(colorSetting->cbBackgroundColor->currentText());
|
||||
}
|
||||
backgroundColor = bgColor.name();
|
||||
}
|
||||
|
||||
// Создаем сервер с полученными настройками
|
||||
m_chatServer = new HttpServerChat(fontList, port, backgroundColor, nullptr);
|
||||
|
||||
// Запускаем сервер
|
||||
if (!m_chatServer->start()) {
|
||||
QMessageBox::warning(this, "Ошибка",
|
||||
QString("Не удалось запустить сервер чата на порту %1").arg(port));
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateChat::onBtnTestClicked()
|
||||
{
|
||||
// Для теста создаем временный сервер или используем существующий
|
||||
if (!m_chatServer || !m_isEditMode) {
|
||||
// Если нет сервера или это не режим редактирования, создаем временный
|
||||
if (m_chatServer && !m_isEditMode) {
|
||||
m_chatServer->stop();
|
||||
delete m_chatServer;
|
||||
}
|
||||
createServer();
|
||||
}
|
||||
|
||||
if (!m_chatServer) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не удалось создать сервер");
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем тестовое сообщение
|
||||
createTestMessage(true);
|
||||
}
|
||||
|
||||
void FCreateChat::onBtnAddClicked()
|
||||
{
|
||||
if (m_isEditMode && m_chatServer) {
|
||||
FSettingsWS *settingsWS = ui->widget;
|
||||
FColorSetting *colorSetting = ui->wBlock;
|
||||
FFontSetting *fontSetting = ui->wFont;
|
||||
|
||||
if (!settingsWS || !colorSetting || !fontSetting) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не найдены настройки сервера");
|
||||
return;
|
||||
}
|
||||
|
||||
int newPort = settingsWS->sbPort->value();
|
||||
bool portChanged = (newPort != m_chatServer->port());
|
||||
|
||||
if (portChanged) {
|
||||
m_chatServer->stop();
|
||||
delete m_chatServer;
|
||||
m_chatServer = nullptr;
|
||||
createServer();
|
||||
}
|
||||
|
||||
if (!m_chatServer) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не удалось обновить сервер");
|
||||
return;
|
||||
}
|
||||
|
||||
// Применяем все настройки
|
||||
QColor bgColor = colorSetting->cbBackgroundColor->currentData().value<QColor>();
|
||||
if (!bgColor.isValid()) {
|
||||
bgColor = QColor(colorSetting->cbBackgroundColor->currentText());
|
||||
}
|
||||
m_chatServer->changeBackground(bgColor.name());
|
||||
|
||||
m_chatServer->setBlockColor(colorSetting->cbBlockColor->currentText());
|
||||
m_chatServer->setBorderColor(colorSetting->cbBorderColor->currentText());
|
||||
m_chatServer->setBorderSize(colorSetting->sbBorderSize->value());
|
||||
m_chatServer->setPadding(colorSetting->sbPadding->value());
|
||||
m_chatServer->setTransparency(colorSetting->hsBlockTransparant->value());
|
||||
|
||||
m_chatServer->setFontFamily(fontSetting->cbFontStyle->currentText());
|
||||
m_chatServer->setFontSize(fontSetting->sbFontSize->value());
|
||||
m_chatServer->setFontColor(fontSetting->cbFontColor->currentText());
|
||||
bool freez = settingsWS->cbFreez->isChecked();
|
||||
m_chatServer->setFreez(freez);
|
||||
|
||||
// Удаление по времени = НЕ freez
|
||||
bool deleteByTime = !freez;
|
||||
int maxCount = settingsWS->sbCount->value();
|
||||
m_chatServer->setDeleteMode(deleteByTime, maxCount);
|
||||
m_chatServer->setMessageTimeout(settingsWS->sbTime->value());
|
||||
|
||||
QString newName = ui->lineEdit->text();
|
||||
if (newName.isEmpty()) {
|
||||
newName = QString("Чат сервер (порт %1)").arg(m_chatServer->port());
|
||||
}
|
||||
|
||||
emit serverUpdated(m_chatServer, newName);
|
||||
accept();
|
||||
} else {
|
||||
// Режим создания
|
||||
createServer();
|
||||
|
||||
if (!m_chatServer) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не удалось создать сервер");
|
||||
return;
|
||||
}
|
||||
|
||||
createTestMessage(false);
|
||||
|
||||
QString name = ui->lineEdit->text();
|
||||
if (name.isEmpty()) {
|
||||
name = QString("Чат сервер (порт %1)").arg(m_chatServer->port());
|
||||
}
|
||||
|
||||
emit serverCreated(m_chatServer, name);
|
||||
m_chatServer = nullptr;
|
||||
accept();
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateChat::createTestMessage(bool isTest)
|
||||
{
|
||||
if (!m_chatServer) {
|
||||
QMessageBox::warning(this, "Ошибка", "Сервер чата не запущен");
|
||||
return;
|
||||
}
|
||||
|
||||
StyleChat style;
|
||||
|
||||
// Получаем настройки из виджетов
|
||||
|
||||
// 1. Получаем настройки цвета блока из FColorSetting
|
||||
FColorSetting *colorSetting = ui->wBlock;
|
||||
if (colorSetting) {
|
||||
QColor blockColor = colorSetting->cbBlockColor->currentData().value<QColor>();
|
||||
if (!blockColor.isValid()) {
|
||||
blockColor = QColor(colorSetting->cbBlockColor->currentText());
|
||||
}
|
||||
style.blockColor = blockColor.name();
|
||||
|
||||
QColor borderColor = colorSetting->cbBorderColor->currentData().value<QColor>();
|
||||
if (!borderColor.isValid()) {
|
||||
borderColor = QColor(colorSetting->cbBorderColor->currentText());
|
||||
}
|
||||
style.borderColor = borderColor.name();
|
||||
|
||||
style.borderSize = colorSetting->sbBorderSize->value();
|
||||
style.padding = colorSetting->sbPadding->value();
|
||||
|
||||
// Прозрачность
|
||||
style.transparency = colorSetting->hsBlockTransparant->value();
|
||||
|
||||
// Цвет фона страницы
|
||||
QColor pageBackgroundColor = colorSetting->cbBackgroundColor->currentData().value<QColor>();
|
||||
if (!pageBackgroundColor.isValid()) {
|
||||
pageBackgroundColor = QColor(colorSetting->cbBackgroundColor->currentText());
|
||||
}
|
||||
style.bColor = pageBackgroundColor.name();
|
||||
|
||||
// Обновляем цвет фона в сервере
|
||||
m_chatServer->changeBackground(style.bColor);
|
||||
}
|
||||
|
||||
// 2. Получаем настройки шрифта из FFontSetting
|
||||
FFontSetting *fontSetting = ui->wFont;
|
||||
if (fontSetting) {
|
||||
style.fontFamily = fontSetting->cbFontStyle->currentText();
|
||||
style.fontSize = fontSetting->sbFontSize->value();
|
||||
|
||||
QColor fontColor = fontSetting->cbFontColor->currentData().value<QColor>();
|
||||
if (!fontColor.isValid()) {
|
||||
fontColor = QColor(fontSetting->cbFontColor->currentText());
|
||||
}
|
||||
style.fontColor = fontColor.name();
|
||||
}
|
||||
|
||||
// 3. Получаем настройки из FSettingsWS
|
||||
FSettingsWS *settingsWS = ui->widget;
|
||||
if (settingsWS) {
|
||||
// Используем sbTime как время отображения сообщения
|
||||
style.timeMsg = settingsWS->sbTime->value();
|
||||
|
||||
// Получаем состояние чекбокса "Вечно" (freez)
|
||||
style.freez = settingsWS->cbFreez->isChecked();
|
||||
|
||||
|
||||
// Настраиваем режим удаления
|
||||
bool deleteByTime = !settingsWS->cbFreez->isChecked(); // Если "Вечно" выбрано, то НЕ удаляем по времени
|
||||
int maxCount = settingsWS->sbCount->value();
|
||||
m_chatServer->setDeleteMode(deleteByTime, maxCount);
|
||||
}
|
||||
|
||||
// Заполняем тестовые данные
|
||||
style.nick = isTest ? "Тестовый пользователь" : "Пользователь";
|
||||
style.context = ui->lineEdit->text().isEmpty() ?
|
||||
(isTest ? "Это тестовое сообщение!" : "Новое сообщение") :
|
||||
ui->lineEdit->text();
|
||||
|
||||
// Добавляем сообщение в сервер
|
||||
m_chatServer->addMessage(style);
|
||||
|
||||
// Очищаем поле ввода, если это не тест
|
||||
if (!isTest) {
|
||||
ui->lineEdit->clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef FCREATECHAT_H
|
||||
#define FCREATECHAT_H
|
||||
|
||||
#include "webserverchat.h"
|
||||
#include <QDialog>
|
||||
|
||||
namespace Ui {
|
||||
class FCreateChat;
|
||||
}
|
||||
|
||||
class FCreateChat : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void serverCreated(HttpServerChat *server, const QString &name);
|
||||
void serverUpdated(HttpServerChat *server, const QString &name);
|
||||
|
||||
public:
|
||||
explicit FCreateChat(QWidget *parent = nullptr);
|
||||
~FCreateChat();
|
||||
HttpServerChat *m_chatServer;
|
||||
|
||||
StyleChat *m_StyleChat;
|
||||
void loadExistingServer(HttpServerChat *server, const QString &name);
|
||||
void setEditMode(bool isEditMode);
|
||||
|
||||
private slots:
|
||||
void onBtnTestClicked();
|
||||
void onBtnAddClicked();
|
||||
|
||||
private:
|
||||
Ui::FCreateChat *ui;
|
||||
bool m_isEditMode;
|
||||
QString m_existingServerName;
|
||||
void createServer();
|
||||
void createTestMessage(bool isTest = false);
|
||||
|
||||
};
|
||||
|
||||
#endif // FCREATECHAT_H
|
||||
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FCreateChat</class>
|
||||
<widget class="QDialog" name="FCreateChat">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>753</width>
|
||||
<height>270</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>0</y>
|
||||
<width>351</width>
|
||||
<height>151</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Блок</string>
|
||||
</property>
|
||||
<widget class="FColorSetting" name="wBlock" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>121</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>150</y>
|
||||
<width>351</width>
|
||||
<height>111</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Шрифт</string>
|
||||
</property>
|
||||
<widget class="FFontSetting" name="wFont" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>81</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>0</y>
|
||||
<width>351</width>
|
||||
<height>151</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Настройки</string>
|
||||
</property>
|
||||
<widget class="FSettingsWS" name="widget" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>121</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>150</y>
|
||||
<width>131</width>
|
||||
<height>20</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Тестовое сообщение</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLineEdit" name="lineEdit">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>170</y>
|
||||
<width>231</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="btnTest">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>610</x>
|
||||
<y>170</y>
|
||||
<width>75</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Отправить</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="btnAdd">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>200</y>
|
||||
<width>75</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Создать</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>FColorSetting</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">fcolorsetting.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FFontSetting</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">ffontsetting.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FSettingsWS</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">fsettingsws.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,338 @@
|
||||
#include "fcreatenotify.h"
|
||||
#include "ui_fcreatenotify.h"
|
||||
#include <QFileDialog>
|
||||
#include <QDebug>
|
||||
#include <QMessageBox>
|
||||
#include <QDir>
|
||||
#include <QCloseEvent>
|
||||
#include <QDesktopServices>
|
||||
#include <QApplication>
|
||||
|
||||
FCreateNotify::FCreateNotify(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::FCreateNotify),
|
||||
m_server(nullptr),
|
||||
m_notificationCounter(0)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Создаем структуру папок для статических файлов
|
||||
QDir appDir(QApplication::applicationDirPath());
|
||||
appDir.mkpath("sounds");
|
||||
appDir.mkpath("fonts");
|
||||
appDir.mkpath("imgs");
|
||||
|
||||
// Заполняем ComboBox событиями
|
||||
QStringList events = {
|
||||
"donation", "follow", "subscription", "raid",
|
||||
"bits", "host", "merch", "goal", "poll", "prediction"
|
||||
};
|
||||
ui->cbEvent->addItems(events);
|
||||
ui->cbEvent->setCurrentIndex(0);
|
||||
// Устанавливаем значения по умолчанию
|
||||
ui->edtHeader->setText("Тестовый пользователь");
|
||||
ui->edtMessage->setText("Это тестовое уведомление!");
|
||||
|
||||
// Включаем кнопки
|
||||
ui->btnTest->setEnabled(true);
|
||||
ui->btnAdd->setEnabled(true);
|
||||
|
||||
// Информация для пользователя
|
||||
ui->btnTest->setToolTip("Создать тестовое уведомление и открыть браузер");
|
||||
ui->btnAdd->setToolTip("Добавить уведомление для отображения");
|
||||
m_server = new HttpServer(nullptr);
|
||||
}
|
||||
|
||||
FCreateNotify::~FCreateNotify()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void FCreateNotify::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
// Останавливаем сервер при закрытии окна
|
||||
if (m_server) {
|
||||
m_server->stop();
|
||||
}
|
||||
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
|
||||
void FCreateNotify::createServer()
|
||||
{
|
||||
if (m_server) {
|
||||
m_server->stop();
|
||||
delete m_server;
|
||||
m_server = nullptr;
|
||||
}
|
||||
|
||||
// Получаем настройки из формы
|
||||
FSettingsWS *settingsWS = ui->widget;
|
||||
if (!settingsWS) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не найдены настройки сервера");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем порт из настроек
|
||||
int port = settingsWS->sbPort->value();
|
||||
|
||||
// Создаем сервер с полученным портом
|
||||
m_server = new HttpServer( this);
|
||||
|
||||
// Запускаем сервер
|
||||
if (m_server->start(port)) {
|
||||
} else {
|
||||
QMessageBox::warning(this, "Ошибка",
|
||||
QString("Не удалось запустить сервер уведомлений на порту %1").arg(port));
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateNotify::on_btnTest_clicked()
|
||||
{
|
||||
// Создаем сервер с текущими настройками
|
||||
createServer();
|
||||
|
||||
if (!m_server) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не удалось создать сервер");
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем тестовое уведомление
|
||||
createNotification(true);
|
||||
}
|
||||
|
||||
void FCreateNotify::on_btnAdd_clicked()
|
||||
{
|
||||
// Создаем сервер с текущими настройками
|
||||
createServer();
|
||||
|
||||
if (!m_server) {
|
||||
QMessageBox::warning(this, "Ошибка", "Не удалось создать сервер");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем название сервера
|
||||
QString name = QString("Уведомления (порт %1)").arg(m_server->port());
|
||||
|
||||
// Отправляем сигнал с созданным сервером
|
||||
emit serverCreated(m_server, name);
|
||||
|
||||
// Закрываем окно
|
||||
accept();
|
||||
}
|
||||
|
||||
void FCreateNotify::createNotification(bool isTest)
|
||||
{
|
||||
if (!m_server) {
|
||||
QMessageBox::warning(this, "Ошибка", "Сервер не запущен");
|
||||
return;
|
||||
}
|
||||
|
||||
m_notificationCounter++;
|
||||
|
||||
Notification notif;
|
||||
|
||||
// Заголовок
|
||||
notif.nickname = ui->edtHeader->text().isEmpty() ?
|
||||
QString("Тест %1").arg(m_notificationCounter) :
|
||||
ui->edtHeader->text();
|
||||
|
||||
// Сообщение
|
||||
notif.content = ui->edtMessage->text().isEmpty() ?
|
||||
QString("Тестовое сообщение #%1").arg(m_notificationCounter) :
|
||||
ui->edtMessage->text();
|
||||
|
||||
// Добавляем тип события
|
||||
notif.content = QString("[%1] %2")
|
||||
.arg(ui->cbEvent->currentText())
|
||||
.arg(notif.content);
|
||||
|
||||
// Картинка
|
||||
QString imgPath = ui->edtFileImg->text();
|
||||
if (!imgPath.isEmpty() && QFile::exists(imgPath)) {
|
||||
QFileInfo imgInfo(imgPath);
|
||||
QString destFileName = imgInfo.fileName();
|
||||
QString destPath = getAbsolutePath("imgs/" + destFileName);
|
||||
|
||||
// Копируем, если файл еще не существует
|
||||
if (!QFile::exists(destPath)) {
|
||||
QFile::copy(imgPath, destPath);
|
||||
}
|
||||
|
||||
notif.url = "/imgs/" + destFileName;
|
||||
}
|
||||
|
||||
// Звук
|
||||
QString soundPath = ui->edtFileSong->text();
|
||||
if (!soundPath.isEmpty() && QFile::exists(soundPath)) {
|
||||
QFileInfo soundInfo(soundPath);
|
||||
QString destFileName = soundInfo.fileName();
|
||||
QString destPath = getAbsolutePath("sounds/" + destFileName);
|
||||
|
||||
// Копируем, если файл еще не существует
|
||||
if (!QFile::exists(destPath)) {
|
||||
QFile::copy(soundPath, destPath);
|
||||
}
|
||||
|
||||
notif.soundURL = "/sounds/" + destFileName;
|
||||
}
|
||||
|
||||
// ПОЛУЧАЕМ НАСТРОЙКИ ИЗ ВИДЖЕТОВ
|
||||
|
||||
// 1. Получаем настройки цвета блока из FColorSetting
|
||||
FColorSetting *colorSetting = ui->wBlock;
|
||||
if (colorSetting) {
|
||||
// Получаем выбранный цвет блока
|
||||
QColor blockColor = colorSetting->cbBlockColor->currentData().value<QColor>();
|
||||
if (!blockColor.isValid()) {
|
||||
blockColor = QColor(colorSetting->cbBlockColor->currentText());
|
||||
}
|
||||
|
||||
// ПРИМЕНЯЕМ ПРОЗРАЧНОСТЬ БЛОКА УВЕДОМЛЕНИЯ
|
||||
int blockTransparency = colorSetting->hsBlockTransparant->value(); // 0-100
|
||||
// Преобразуем 0-100 в 0-255
|
||||
int alpha = (blockTransparency * 255) / 100;
|
||||
blockColor.setAlpha(alpha);
|
||||
|
||||
notif.blockColor = blockColor.name(QColor::HexArgb); // Включаем альфа-канал
|
||||
|
||||
// Получаем выбранный цвет границы
|
||||
QColor borderColor = colorSetting->cbBorderColor->currentData().value<QColor>();
|
||||
if (!borderColor.isValid()) {
|
||||
borderColor = QColor(colorSetting->cbBorderColor->currentText());
|
||||
}
|
||||
notif.borderColor = borderColor.name();
|
||||
|
||||
// Получаем размер границы
|
||||
notif.borderSize = colorSetting->sbBorderSize->value();
|
||||
|
||||
// Получаем цвет фона СТРАНИЦЫ (не блока!)
|
||||
QColor pageBackgroundColor = colorSetting->cbBackgroundColor->currentData().value<QColor>();
|
||||
if (!pageBackgroundColor.isValid()) {
|
||||
pageBackgroundColor = QColor(colorSetting->cbBackgroundColor->currentText());
|
||||
}
|
||||
notif.pageBackgroundColor = pageBackgroundColor.name();
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 2. Получаем настройки шрифта заголовка из FFontSetting
|
||||
FFontSetting *fontHeaderSetting = ui->wFont;
|
||||
if (fontHeaderSetting) {
|
||||
// Получаем выбранный шрифт заголовка
|
||||
notif.titleFamily = fontHeaderSetting->cbFontStyle->currentText();
|
||||
|
||||
// Получаем размер шрифта заголовка
|
||||
notif.titleSize = fontHeaderSetting->sbFontSize->value();
|
||||
|
||||
// Получаем цвет шрифта заголовка
|
||||
QColor titleColor = fontHeaderSetting->cbFontColor->currentData().value<QColor>();
|
||||
if (!titleColor.isValid()) {
|
||||
titleColor = QColor(fontHeaderSetting->cbFontColor->currentText());
|
||||
}
|
||||
notif.titleColor = titleColor.name();
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 3. Получаем настройки шрифта сообщения из FFontSetting
|
||||
FFontSetting *fontMessageSetting = ui->wFont_2;
|
||||
if (fontMessageSetting) {
|
||||
// Получаем выбранный шрифт сообщения
|
||||
notif.contentFamily = fontMessageSetting->cbFontStyle->currentText();
|
||||
|
||||
// Получаем размер шрифта сообщения
|
||||
notif.contentSize = fontMessageSetting->sbFontSize->value();
|
||||
|
||||
// Получаем цвет шрифта сообщения
|
||||
QColor contentColor = fontMessageSetting->cbFontColor->currentData().value<QColor>();
|
||||
if (!contentColor.isValid()) {
|
||||
contentColor = QColor(fontMessageSetting->cbFontColor->currentText());
|
||||
}
|
||||
notif.contentColor = contentColor.name();
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 4. Получаем настройки веб-сервера из FSettingsWS
|
||||
FSettingsWS *settingsWS = ui->widget;
|
||||
if (settingsWS) {
|
||||
// Получаем время отображения уведомления
|
||||
notif.duration = settingsWS->sbTime->value();
|
||||
|
||||
} else {
|
||||
// Значение по умолчанию, если виджет не найден
|
||||
notif.duration = 15;
|
||||
}
|
||||
|
||||
// Добавляем уведомление на сервер
|
||||
m_server->addNotification(notif);
|
||||
|
||||
|
||||
|
||||
if (!isTest) {
|
||||
// Очищаем только если это не тест
|
||||
ui->edtHeader->clear();
|
||||
ui->edtMessage->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateNotify::copyFileToAppDir(const QString &sourcePath, const QString &destSubDir)
|
||||
{
|
||||
QFileInfo sourceInfo(sourcePath);
|
||||
QString destPath = getAbsolutePath(destSubDir + "/" + sourceInfo.fileName());
|
||||
|
||||
// Копируем файл, если он еще не существует или изменился
|
||||
if (!QFile::exists(destPath) ||
|
||||
QFileInfo(sourcePath).lastModified() > QFileInfo(destPath).lastModified()) {
|
||||
QFile::remove(destPath); // Удаляем старую версию
|
||||
if (QFile::copy(sourcePath, destPath)) {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString FCreateNotify::getAbsolutePath(const QString &relativePath)
|
||||
{
|
||||
return QApplication::applicationDirPath() + "/" + relativePath;
|
||||
}
|
||||
|
||||
void FCreateNotify::on_btnOpenImg_clicked()
|
||||
{
|
||||
QString fileName = QFileDialog::getOpenFileName(this,
|
||||
"Выберите изображение",
|
||||
"",
|
||||
"Images (*.png *.jpg *.jpeg *.gif *.bmp *.svg)");
|
||||
|
||||
if (!fileName.isEmpty()) {
|
||||
ui->edtFileImg->setText(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateNotify::on_btnOpenSong_clicked()
|
||||
{
|
||||
QString fileName = QFileDialog::getOpenFileName(this,
|
||||
"Выберите звуковой файл",
|
||||
"",
|
||||
"Audio files (*.mp3 *.wav *.ogg *.flac)");
|
||||
|
||||
if (!fileName.isEmpty()) {
|
||||
ui->edtFileSong->setText(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
void FCreateNotify::onServerStarted(bool success)
|
||||
{
|
||||
if (success) {
|
||||
ui->btnTest->setEnabled(true);
|
||||
ui->btnAdd->setEnabled(true);
|
||||
|
||||
QString url = QString("http://localhost:%1").arg(m_server->port());
|
||||
ui->btnTest->setText("Тест (" + url + ")");
|
||||
|
||||
} else {
|
||||
ui->btnTest->setEnabled(false);
|
||||
ui->btnAdd->setEnabled(false);
|
||||
QMessageBox::warning(this, "Ошибка", "Не удалось запустить веб-сервер");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef FCREATENOTIFY_H
|
||||
#define FCREATENOTIFY_H
|
||||
|
||||
#include "webservernotify.h"
|
||||
#include <QDialog>
|
||||
|
||||
namespace Ui {
|
||||
class FCreateNotify;
|
||||
}
|
||||
|
||||
class FCreateNotify : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void serverCreated(HttpServer *server, const QString &name);
|
||||
|
||||
public:
|
||||
explicit FCreateNotify(QWidget *parent = nullptr);
|
||||
~FCreateNotify();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void on_btnOpenImg_clicked();
|
||||
void on_btnOpenSong_clicked();
|
||||
void on_btnTest_clicked();
|
||||
void on_btnAdd_clicked();
|
||||
void onServerStarted(bool success);
|
||||
|
||||
private:
|
||||
Ui::FCreateNotify *ui;
|
||||
void createNotification(bool isTest = false);
|
||||
QString getAbsolutePath(const QString &relativePath);
|
||||
void copyFileToAppDir(const QString &sourcePath, const QString &destSubDir);
|
||||
|
||||
// Сервер
|
||||
HttpServer *m_server;
|
||||
int m_notificationCounter;
|
||||
void createServer();
|
||||
};
|
||||
|
||||
#endif // FCREATENOTIFY_H
|
||||
@@ -0,0 +1,252 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FCreateNotify</class>
|
||||
<widget class="QDialog" name="FCreateNotify">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>728</width>
|
||||
<height>439</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>0</y>
|
||||
<width>351</width>
|
||||
<height>151</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Настройки</string>
|
||||
</property>
|
||||
<widget class="FSettingsWS" name="widget" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>121</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>0</y>
|
||||
<width>351</width>
|
||||
<height>151</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Блок</string>
|
||||
</property>
|
||||
<widget class="FColorSetting" name="wBlock" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>121</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>150</y>
|
||||
<width>351</width>
|
||||
<height>141</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Заголовок</string>
|
||||
</property>
|
||||
<widget class="FFontSetting" name="wFont" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>50</y>
|
||||
<width>331</width>
|
||||
<height>81</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLineEdit" name="edtHeader">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>[NICK] </string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>290</y>
|
||||
<width>351</width>
|
||||
<height>141</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Сообщение</string>
|
||||
</property>
|
||||
<widget class="FFontSetting" name="wFont_2" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>50</y>
|
||||
<width>331</width>
|
||||
<height>81</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLineEdit" name="edtMessage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>331</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Сообщение</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>150</y>
|
||||
<width>351</width>
|
||||
<height>181</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Персональное</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="gridLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>9</x>
|
||||
<y>19</y>
|
||||
<width>331</width>
|
||||
<height>152</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLineEdit" name="edtFileSong"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Звук</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="btnOpenSong">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLineEdit" name="edtFileImg"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Картинка</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="btnOpenImg">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Событие</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QComboBox" name="cbEvent"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="btnTest">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>370</x>
|
||||
<y>340</y>
|
||||
<width>80</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Тест</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="btnAdd">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>460</x>
|
||||
<y>340</y>
|
||||
<width>81</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Создать</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>FColorSetting</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">fcolorsetting.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FFontSetting</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">ffontsetting.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FSettingsWS</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">fsettingsws.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "ffontsetting.h"
|
||||
#include "qdebug.h"
|
||||
#include "qdir.h"
|
||||
#include "ui_ffontsetting.h"
|
||||
#include <QFontDatabase>
|
||||
#include <QApplication>
|
||||
|
||||
FFontSetting::FFontSetting(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, cbFontStyle(nullptr)
|
||||
, cbFontColor(nullptr)
|
||||
, sbFontSize(nullptr)
|
||||
, ui(new Ui::FFontSetting)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setupColorComboBox(ui->cbFontColor);
|
||||
cbFontStyle = ui->cbFontStyle;
|
||||
cbFontColor = ui->cbFontColor;
|
||||
sbFontSize = ui->sbFontSize;
|
||||
|
||||
// Загружаем шрифты при создании виджета
|
||||
loadFonts();
|
||||
}
|
||||
|
||||
FFontSetting::~FFontSetting()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void FFontSetting::setupColorComboBox(QComboBox* comboBox)
|
||||
{
|
||||
comboBox->clear();
|
||||
|
||||
// Добавляем цвета
|
||||
QStringList colorNames = QColor::colorNames();
|
||||
foreach (const QString &name, colorNames) {
|
||||
QColor color(name);
|
||||
QPixmap pixmap(20, 20);
|
||||
pixmap.fill(color);
|
||||
comboBox->addItem(QIcon(pixmap), name, color);
|
||||
}
|
||||
|
||||
// Устанавливаем первый цвет по умолчанию
|
||||
if (comboBox->count() > 0) {
|
||||
comboBox->setCurrentIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
void FFontSetting::loadFonts()
|
||||
{
|
||||
QString fontsPath = "fonts";
|
||||
QDir fontsDir(fontsPath);
|
||||
|
||||
if (fontsDir.exists()) {
|
||||
QStringList fontFiles = fontsDir.entryList(QStringList() << "*.ttf" << "*.otf" << "*.ttc",
|
||||
QDir::Files);
|
||||
|
||||
|
||||
foreach (const QString &fontFile, fontFiles) {
|
||||
QString fontPath = fontsDir.absoluteFilePath(fontFile);
|
||||
int fontId = QFontDatabase::addApplicationFont(fontPath);
|
||||
|
||||
if (fontId != -1) {
|
||||
QStringList fontFamilies = QFontDatabase::applicationFontFamilies(fontId);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
// ОБНОВЛЯЕМ СПИСОК ШРИФТОВ В КОМБОБОКСЕ
|
||||
ui->cbFontStyle->clear(); // Очищаем старый список
|
||||
|
||||
// Получаем все шрифты через объект QFontDatabase
|
||||
QFontDatabase fontDatabase;
|
||||
QStringList fontFamilies = fontDatabase.families(); // Получаем все шрифты
|
||||
|
||||
// ИЛИ используйте статический метод с параметром:
|
||||
// QStringList fontFamilies = QFontDatabase::families(QFontDatabase::Any);
|
||||
|
||||
ui->cbFontStyle->addItems(fontFamilies); // Добавляем все шрифты
|
||||
|
||||
// Устанавливаем шрифт по умолчанию
|
||||
int defaultIndex = ui->cbFontStyle->findText("Arial");
|
||||
if (defaultIndex >= 0) {
|
||||
ui->cbFontStyle->setCurrentIndex(defaultIndex);
|
||||
}
|
||||
|
||||
// Показываем количество доступных шрифтов
|
||||
} else {
|
||||
|
||||
// Загружаем системные шрифты, если папки нет
|
||||
QFontDatabase fontDatabase;
|
||||
QStringList fontFamilies = fontDatabase.families();
|
||||
ui->cbFontStyle->clear();
|
||||
ui->cbFontStyle->addItems(fontFamilies);
|
||||
|
||||
int defaultIndex = ui->cbFontStyle->findText("Arial");
|
||||
if (defaultIndex >= 0) {
|
||||
ui->cbFontStyle->setCurrentIndex(defaultIndex);
|
||||
}
|
||||
|
||||
// Проверяем альтернативные пути
|
||||
QString appDir = QApplication::applicationDirPath();
|
||||
QString altPath = appDir + "/fonts";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef FFONTSETTING_H
|
||||
#define FFONTSETTING_H
|
||||
|
||||
#include "qspinbox.h"
|
||||
#include <QWidget>
|
||||
#include <QFontComboBox>
|
||||
#include <QComboBox>
|
||||
|
||||
namespace Ui {
|
||||
class FFontSetting;
|
||||
}
|
||||
|
||||
class FFontSetting : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FFontSetting(QWidget *parent = nullptr);
|
||||
~FFontSetting();
|
||||
QFontComboBox *cbFontStyle;
|
||||
QComboBox *cbFontColor;
|
||||
QSpinBox *sbFontSize;
|
||||
|
||||
private:
|
||||
Ui::FFontSetting *ui;
|
||||
|
||||
void setupColorComboBox(QComboBox* comboBox);
|
||||
void loadFonts(); // Убедитесь, что метод объявлен как public или private
|
||||
};
|
||||
|
||||
#endif // FFONTSETTING_H
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FFontSetting</class>
|
||||
<widget class="QWidget" name="FFontSetting">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>334</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="gridLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>331</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Стиль</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Цвет</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QFontComboBox" name="cbFontStyle"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="cbFontColor"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Размер</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QSpinBox" name="sbFontSize">
|
||||
<property name="value">
|
||||
<number>14</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "fsettingsws.h"
|
||||
#include "ui_fsettingsws.h"
|
||||
|
||||
FSettingsWS::FSettingsWS(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, sbPort(nullptr)
|
||||
, sbTime(nullptr)
|
||||
, sbCount(nullptr)
|
||||
, cbFreez(nullptr)
|
||||
, ui(new Ui::FSettingsWS)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
sbPort = ui->sbPort;
|
||||
sbTime = ui->sbTime;
|
||||
sbCount = ui->sbCount;
|
||||
cbFreez = ui->cbFreez;
|
||||
}
|
||||
|
||||
FSettingsWS::~FSettingsWS()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef FSETTINGSWS_H
|
||||
#define FSETTINGSWS_H
|
||||
|
||||
#include "qcheckbox.h"
|
||||
#include "qspinbox.h"
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ui {
|
||||
class FSettingsWS;
|
||||
}
|
||||
|
||||
class FSettingsWS : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FSettingsWS(QWidget *parent = nullptr);
|
||||
~FSettingsWS();
|
||||
QSpinBox *sbPort;
|
||||
QSpinBox *sbTime;
|
||||
QSpinBox *sbCount;
|
||||
QCheckBox *cbFreez;
|
||||
|
||||
private:
|
||||
Ui::FSettingsWS *ui;
|
||||
};
|
||||
|
||||
#endif // FSETTINGSWS_H
|
||||
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FSettingsWS</class>
|
||||
<widget class="QWidget" name="FSettingsWS">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>402</width>
|
||||
<height>132</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="gridLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>401</width>
|
||||
<height>131</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Время отображения</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Максимальное количество</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="sbTime">
|
||||
<property name="value">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="sbPort">
|
||||
<property name="minimum">
|
||||
<number>8000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Порт</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="sbCount">
|
||||
<property name="value">
|
||||
<number>5</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="cbFreez">
|
||||
<property name="text">
|
||||
<string>Вечно</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "fsinglegrid.h"
|
||||
#include "ui_fsinglegrid.h"
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
#include <QFileDialog>
|
||||
|
||||
FSingleGrid::FSingleGrid(QWidget *parent) :
|
||||
QWidget(parent),
|
||||
ui(new Ui::FSingleGrid)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
|
||||
}
|
||||
|
||||
FSingleGrid::~FSingleGrid()
|
||||
{
|
||||
delete ui;
|
||||
|
||||
|
||||
}
|
||||
|
||||
QTableWidget* FSingleGrid::tableWidget() const
|
||||
{
|
||||
return ui->sg;
|
||||
}
|
||||
|
||||
void FSingleGrid::initForm(QString aBlockName, QString aNewName, bool btnOpen)
|
||||
{
|
||||
ui->groupBox->setTitle(aBlockName);
|
||||
QStringList headers;
|
||||
headers << "Название" << "Файл";
|
||||
ui->sg->setHorizontalHeaderLabels(headers);
|
||||
ui->sg->setSelectionBehavior(QAbstractItemView::SelectRows); // Выделение строк
|
||||
ui->sg->setSelectionMode(QAbstractItemView::SingleSelection); // Одиночное выделение
|
||||
ui->sg->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
ui->sg->setColumnWidth(0, 100);
|
||||
ui->sg->setColumnWidth(1, 170);
|
||||
ui->sg->setObjectName(aNewName);
|
||||
ui->btnOpen->setVisible(btnOpen);
|
||||
|
||||
}
|
||||
|
||||
void FSingleGrid::toGrid(QString aName, QString aFile)
|
||||
{
|
||||
// Добавляем новую строку в таблицу
|
||||
int row = ui->sg->rowCount();
|
||||
ui->sg->insertRow(row);
|
||||
QTableWidgetItem *R1 = new QTableWidgetItem(aName);
|
||||
QTableWidgetItem *R2 = new QTableWidgetItem(aFile);
|
||||
ui->sg->setItem(row, 0, R1);
|
||||
ui->sg->setItem(row, 1, R2);
|
||||
ui->sg->scrollToBottom();
|
||||
}
|
||||
|
||||
void FSingleGrid::on_btnAdd_clicked()
|
||||
{
|
||||
toGrid(ui->edtName->text(), ui->edtFileName->text());
|
||||
db->SaveTableWidget(ui->sg);
|
||||
}
|
||||
|
||||
void FSingleGrid::setDatabase(uDataBase *database)
|
||||
{
|
||||
db = database;
|
||||
}
|
||||
|
||||
void FSingleGrid::on_btnDel_clicked()
|
||||
{
|
||||
// Проверяем, есть ли выделенная строка
|
||||
if (!ui->sg->currentItem()) {
|
||||
QMessageBox::warning(this, "Внимание", "Выберите строку для удаления!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем индекс выделенной строки
|
||||
int row = ui->sg->currentItem()->row();
|
||||
|
||||
// Удаляем строку
|
||||
ui->sg->removeRow(row);
|
||||
db->SaveTableWidget(ui->sg);
|
||||
}
|
||||
|
||||
|
||||
void FSingleGrid::on_btnEdt_clicked()
|
||||
{
|
||||
// Проверяем, есть ли выделенная строка
|
||||
if (!ui->sg->currentItem()) {
|
||||
QMessageBox::warning(this, "Внимание", "Выберите строку для редактирования!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем индекс выделенной строки
|
||||
int row = ui->sg->currentItem()->row();
|
||||
|
||||
// Получаем данные из выбранной строки
|
||||
ui->sg->item(row, 0)->setText(ui->edtName->text());
|
||||
ui->sg->item(row, 1)->setText(ui->edtFileName->text());
|
||||
db->SaveTableWidget(ui->sg);
|
||||
}
|
||||
|
||||
|
||||
void FSingleGrid::on_btnOpen_clicked()
|
||||
{
|
||||
// Диалог выбора файла
|
||||
QString fileName = QFileDialog::getOpenFileName(
|
||||
this, // родительское окно
|
||||
"Выберите файл", // заголовок окна
|
||||
QDir::homePath(), // начальная директория
|
||||
"Все файлы (*.*);;" // фильтры файлов
|
||||
"Текстовые файлы (*.txt);;"
|
||||
"Аудио (*.mp3);;"
|
||||
"Приложения (*.exe *.bat *.cmd)"
|
||||
);
|
||||
|
||||
// Если файл выбран (не нажата кнопка "Отмена")
|
||||
if (!fileName.isEmpty()) {
|
||||
// Записываем путь к файлу в поле ввода
|
||||
ui->edtFileName->setText(fileName);
|
||||
|
||||
// Опционально: автоматически заполняем поле имени из названия файла
|
||||
if (ui->edtName->text().isEmpty()) {
|
||||
QFileInfo fileInfo(fileName);
|
||||
QString baseName = fileInfo.baseName(); // Имя файла без расширения
|
||||
ui->edtName->setText(baseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FSingleGrid::on_sg_cellClicked(int row, int column)
|
||||
{
|
||||
ui->edtName->setText(ui->sg->item(row,0)->text());
|
||||
ui->edtFileName->setText(ui->sg->item(row,1)->text());
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef FSINGLEGRID_H
|
||||
#define FSINGLEGRID_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTableWidget>
|
||||
#include "udatabase.h"
|
||||
|
||||
namespace Ui {
|
||||
class FSingleGrid;
|
||||
}
|
||||
|
||||
class FSingleGrid : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FSingleGrid(QWidget *parent = nullptr);
|
||||
~FSingleGrid();
|
||||
void initForm(QString aBlockName, QString aNewName, bool btnOpen = false);
|
||||
void toGrid(QString aName, QString aFile);
|
||||
QTableWidget* tableWidget() const;
|
||||
void setDatabase(uDataBase *database);
|
||||
private slots:
|
||||
void on_btnAdd_clicked();
|
||||
|
||||
void on_btnDel_clicked();
|
||||
|
||||
void on_btnEdt_clicked();
|
||||
|
||||
void on_btnOpen_clicked();
|
||||
|
||||
void on_sg_cellClicked(int row, int column);
|
||||
|
||||
private:
|
||||
Ui::FSingleGrid *ui;
|
||||
uDataBase *db = nullptr;
|
||||
};
|
||||
|
||||
#endif // FSINGLEGRID_H
|
||||
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FSingleGrid</class>
|
||||
<widget class="QWidget" name="FSingleGrid">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>311</width>
|
||||
<height>234</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>309</width>
|
||||
<height>231</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>GroupBox</string>
|
||||
</property>
|
||||
<widget class="QTableWidget" name="sg">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>5</x>
|
||||
<y>21</y>
|
||||
<width>301</width>
|
||||
<height>131</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<column/>
|
||||
<column/>
|
||||
</widget>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>160</y>
|
||||
<width>291</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="edtName"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="edtFileName"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnOpen">
|
||||
<property name="text">
|
||||
<string>Открыть</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnAdd">
|
||||
<property name="text">
|
||||
<string>Добавить</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnEdt">
|
||||
<property name="text">
|
||||
<string>Изменить</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnDel">
|
||||
<property name="text">
|
||||
<string>Удалить</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,576 @@
|
||||
/*
|
||||
AMOLED Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 01/10/2021, 15:49.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/AMOLED.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:#000000;
|
||||
}
|
||||
QDialog {
|
||||
background-color:#000000;
|
||||
}
|
||||
QColorDialog {
|
||||
background-color:#000000;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color:#000000;
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QPlainTextEdit {
|
||||
selection-background-color:#f39c12;
|
||||
background-color:#000000;
|
||||
border: 1px solid #FF00FF;
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QPushButton{
|
||||
border: 1px transparent;
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QPushButton::default{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #e67e22;
|
||||
border-width: 1px;
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QPushButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #FF00FF;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-radius: 6px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-bottom: 2px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QPushButton:pressed{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #FF00FF;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-radius: 6px;
|
||||
border-style: solid;
|
||||
color: #e67e22;
|
||||
padding-bottom: 1px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QPushButton:disabled{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-radius: 6px;
|
||||
border-style: solid;
|
||||
color: #808086;
|
||||
padding-bottom: 1px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QToolButton {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #e67e22;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QToolButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #e67e22;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-radius: 6px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-bottom: 1px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QLineEdit {
|
||||
border-width: 1px; border-radius: 4px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
padding: 0 8px;
|
||||
color: #a9b7c6;
|
||||
background:#000000;
|
||||
selection-background-color:#007b50;
|
||||
selection-color: #FFFFFF;
|
||||
}
|
||||
QLabel {
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QLCDNumber {
|
||||
color: #e67e22;
|
||||
}
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: rgb(240, 240, 240);
|
||||
border-width: 1px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
background-color:#000000;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #e67e22;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QMenu{
|
||||
background-color:#000000;
|
||||
}
|
||||
QMenuBar {
|
||||
background:rgb(0, 0, 0);
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QMenuBar::item {
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background: transparent;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #e67e22;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-radius: 6px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-bottom: 0px;
|
||||
background-color: #000000;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: #e67e22;
|
||||
border-bottom-color: transparent;
|
||||
border-left-width: 2px;
|
||||
color: #FFFFFF;
|
||||
padding-left:15px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:7px;
|
||||
background-color:#000000;
|
||||
}
|
||||
QMenu::item {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #a9b7c6;
|
||||
padding-left:17px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:7px;
|
||||
background-color:#000000;
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:#000000;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: rgb(77,77,77);
|
||||
background-color:#000000;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #808086;
|
||||
padding: 3px;
|
||||
margin-left:3px;
|
||||
background-color:#000000;
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #e67e22;
|
||||
border-bottom-width: 2px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-left: 3px;
|
||||
padding-bottom: 2px;
|
||||
margin-left:3px;
|
||||
background-color:#000000;
|
||||
}
|
||||
|
||||
QCheckBox {
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
}
|
||||
QCheckBox:disabled {
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
QCheckBox:hover {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
border-width:1px;
|
||||
border-color: rgb(87, 97, 106);
|
||||
background-color:#000000;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #e67e22;
|
||||
color: #a9b7c6;
|
||||
background-color: #e67e22;
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #e67e22;
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QRadioButton {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
padding: 1px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: #e67e22;
|
||||
color: #a9b7c6;
|
||||
background-color: #e67e22;
|
||||
}
|
||||
QRadioButton::indicator:!checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: #e67e22;
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QStatusBar {
|
||||
color:#34e8eb;
|
||||
}
|
||||
QSpinBox {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QDoubleSpinBox {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QTimeEdit {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QDateTimeEdit {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QDateEdit {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QComboBox {
|
||||
color: #a9b7c6;
|
||||
background: #1e1d23;
|
||||
}
|
||||
QComboBox:editable {
|
||||
background: #1e1d23;
|
||||
color: #a9b7c6;
|
||||
selection-background-color:#000000;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
color: #a9b7c6;
|
||||
background: #1e1d23;
|
||||
selection-color: #FFFFFF;
|
||||
selection-background-color:#000000;
|
||||
}
|
||||
QComboBox:!editable:on, QComboBox::drop-down:editable:on {
|
||||
color: #a9b7c6;
|
||||
background: #1e1d23;
|
||||
}
|
||||
QFontComboBox {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QToolBox {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QToolBox::tab {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QToolBox::tab:selected {
|
||||
color: #FFFFFF;
|
||||
background-color:#000000;
|
||||
}
|
||||
QScrollArea {
|
||||
color: #FFFFFF;
|
||||
background-color:#000000;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 5px;
|
||||
background: #e67e22;
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 5px;
|
||||
background: #e67e22;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
|
||||
border: 1px solid #5c5c5c;
|
||||
width: 14px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: qlineargradient(x1:1, y1:1, x2:0, y2:0, stop:0 #b4b4b4, stop:1 #8f8f8f);
|
||||
border: 1px solid #5c5c5c;
|
||||
height: 14px;
|
||||
margin: 0 -5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::add-page:horizontal {
|
||||
background: white;
|
||||
}
|
||||
QSlider::add-page:vertical {
|
||||
background: white;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #e67e22;
|
||||
}
|
||||
QSlider::sub-page:vertical {
|
||||
background: #e67e22;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
max-height: 20px;
|
||||
background: rgb(0,0,0);
|
||||
border: 1px transparent grey;
|
||||
margin: 0px 20px 0px 20px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 0), stop:0.7 rgba(255, 0, 0, 0), stop:0.71 rgb(230, 126, 34), stop:1 rgb(230, 126, 34));
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(0,0,0);
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: rgb(230, 126, 34);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(0,0,0);
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::add-line:horizontal {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 0), stop:0.7 rgba(255, 0, 0, 0), stop:0.71 rgb(230, 126, 34), stop:1 rgb(230, 126, 34));
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:horizontal:hover {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:horizontal:pressed {
|
||||
border: 1px solid;
|
||||
border-color: grey;
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 0), stop:0.7 rgba(255, 0, 0, 0), stop:0.71 rgb(230, 126, 34), stop:1 rgb(230, 126, 34));
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal:hover {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal:pressed {
|
||||
border: 1px solid;
|
||||
border-color: grey;
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::left-arrow:horizontal {
|
||||
border: 1px transparent grey;
|
||||
border-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(0,0,0);
|
||||
}
|
||||
QScrollBar::right-arrow:horizontal {
|
||||
border: 1px transparent grey;
|
||||
border-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(0,0,0);
|
||||
}
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
max-width: 20px;
|
||||
background: rgb(0,0,0);
|
||||
border: 1px transparent grey;
|
||||
margin: 20px 0px 20px 0px;
|
||||
}
|
||||
QScrollBar::add-line:vertical {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 rgba(255, 0, 0, 0), stop:0.7 rgba(255, 0, 0, 0), stop:0.71 rgb(230, 126, 34), stop:1 rgb(230, 126, 34));
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical:hover {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical:pressed {
|
||||
border: 1px solid;
|
||||
border-color: grey;
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 rgba(255, 0, 0, 0), stop:0.7 rgba(255, 0, 0, 0), stop:0.71 rgb(230, 126, 34), stop:1 rgb(230, 126, 34));
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical:hover {
|
||||
border: 1px solid;
|
||||
border-color: rgb(0,0,0);
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical:pressed {
|
||||
border: 1px solid;
|
||||
border-color: grey;
|
||||
border-radius: 8px;
|
||||
background: rgb(230, 126, 34);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 rgba(255, 0, 0, 0), stop:0.7 rgba(255, 0, 0, 0), stop:0.71 rgb(230, 126, 34), stop:1 rgb(230, 126, 34));
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(0,0,0);
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: rgb(230, 126, 34);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(0,0,0);
|
||||
min-heigth: 25px;
|
||||
}
|
||||
QScrollBar::up-arrow:vertical {
|
||||
border: 1px transparent grey;
|
||||
border-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(0,0,0);
|
||||
}
|
||||
QScrollBar::down-arrow:vertical {
|
||||
border: 1px transparent grey;
|
||||
border-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(0,0,0);
|
||||
}
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
/*
|
||||
Aqua Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 22/01/2019, 07:55.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/Aqua.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:#ececec;
|
||||
}
|
||||
QTextEdit {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QPlainTextEdit {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QToolButton {
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QToolButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(195, 195, 195), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(197, 197, 197), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(197, 197, 197));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(195, 195, 195), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QToolButton:pressed{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(142,142,142);
|
||||
}
|
||||
QPushButton{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QPushButton::default{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QPushButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(195, 195, 195), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(197, 197, 197), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(197, 197, 197));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(195, 195, 195), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QPushButton:pressed{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(142,142,142);
|
||||
}
|
||||
QPushButton:disabled{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
background-color: rgb(142,142,142);
|
||||
}
|
||||
QLineEdit {
|
||||
border-width: 1px; border-radius: 4px;
|
||||
border-style: solid;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QLabel {
|
||||
color: #000000;
|
||||
}
|
||||
QLCDNumber {
|
||||
color: rgb(0, 113, 255, 255);
|
||||
}
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: rgb(240, 240, 240);
|
||||
border-width: 1px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(230, 230, 230);
|
||||
border-style: solid;
|
||||
background-color:rgb(207,207,207);
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(49, 147, 250, 255), stop:1 rgba(34, 142, 255, 255));
|
||||
border-radius: 10px;
|
||||
}
|
||||
QMenuBar {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(207, 209, 207, 255), stop:1 rgba(230, 229, 230, 255));
|
||||
}
|
||||
QMenuBar::item {
|
||||
color: #000000;
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(207, 209, 207, 255), stop:1 rgba(230, 229, 230, 255));
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
color: #FFFFFF;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
border-bottom-color: transparent;
|
||||
border-left-width: 2px;
|
||||
color: #000000;
|
||||
padding-left:15px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:7px;
|
||||
}
|
||||
QMenu::item {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-width: 1px;
|
||||
color: #000000;
|
||||
padding-left:17px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:7px;
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:#000000;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: rgb(223,223,223);
|
||||
background-color:rgb(226,226,226);
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QTabBar::tab:first {
|
||||
border-style: solid;
|
||||
border-left-width:1px;
|
||||
border-right-width:0px;
|
||||
border-top-width:1px;
|
||||
border-bottom-width:1px;
|
||||
border-top-color: rgb(209,209,209);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(209, 209, 209, 209), stop:1 rgba(229, 229, 229, 229));
|
||||
border-bottom-color: rgb(229,229,229);
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(247, 247, 247, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QTabBar::tab:last {
|
||||
border-style: solid;
|
||||
border-width:1px;
|
||||
border-top-color: rgb(209,209,209);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(209, 209, 209, 209), stop:1 rgba(229, 229, 229, 229));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(209, 209, 209, 209), stop:1 rgba(229, 229, 229, 229));
|
||||
border-bottom-color: rgb(229,229,229);
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(247, 247, 247, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QTabBar::tab {
|
||||
border-style: solid;
|
||||
border-top-width:1px;
|
||||
border-bottom-width:1px;
|
||||
border-left-width:1px;
|
||||
border-top-color: rgb(209,209,209);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(209, 209, 209, 209), stop:1 rgba(229, 229, 229, 229));
|
||||
border-bottom-color: rgb(229,229,229);
|
||||
color: #000000;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(247, 247, 247, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
border-style: solid;
|
||||
border-left-width:1px;
|
||||
border-right-color: transparent;
|
||||
border-top-color: rgb(209,209,209);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(209, 209, 209, 209), stop:1 rgba(229, 229, 229, 229));
|
||||
border-bottom-color: rgb(229,229,229);
|
||||
color: #FFFFFF;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
|
||||
QTabBar::tab:selected, QTabBar::tab:first:selected, QTabBar::tab:hover {
|
||||
border-style: solid;
|
||||
border-left-width:1px;
|
||||
border-bottom-width:1px;
|
||||
border-top-width:1px;
|
||||
border-right-color: transparent;
|
||||
border-top-color: rgb(209,209,209);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(209, 209, 209, 209), stop:1 rgba(229, 229, 229, 229));
|
||||
border-bottom-color: rgb(229,229,229);
|
||||
color: #FFFFFF;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
|
||||
QCheckBox {
|
||||
color: #000000;
|
||||
padding: 2px;
|
||||
}
|
||||
QCheckBox:disabled {
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
QCheckBox:hover {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
border-width:1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
color: #000000;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
color: #000000;
|
||||
}
|
||||
QRadioButton {
|
||||
color: 000000;
|
||||
padding: 1px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
color: #a9b7c6;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QRadioButton::indicator:!checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QStatusBar {
|
||||
color:#027f7f;
|
||||
}
|
||||
QSpinBox {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QDoubleSpinBox {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QTimeEdit {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QDateTimeEdit {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
QDateEdit {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(0, 113, 255, 255), stop:1 rgba(91, 171, 252, 255));
|
||||
}
|
||||
|
||||
QToolBox {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QToolBox::tab {
|
||||
color: #a9b7c6;
|
||||
background-color:#000000;
|
||||
}
|
||||
QToolBox::tab:selected {
|
||||
color: #FFFFFF;
|
||||
background-color:#000000;
|
||||
}
|
||||
QScrollArea {
|
||||
color: #FFFFFF;
|
||||
background-color:#000000;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 5px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(49, 147, 250, 255), stop:1 rgba(34, 142, 255, 255));
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 5px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(49, 147, 250, 255), stop:1 rgba(34, 142, 255, 255));
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
width: 12px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
height: 12px;
|
||||
margin: 0 -5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::add-page:horizontal {
|
||||
background: rgb(181,181,181);
|
||||
}
|
||||
QSlider::add-page:vertical {
|
||||
background: rgb(181,181,181);
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(49, 147, 250, 255), stop:1 rgba(34, 142, 255, 255));
|
||||
}
|
||||
QSlider::sub-page:vertical {
|
||||
background-color: qlineargradient(spread:pad, y1:0.5, x1:1, y2:0.5, x2:0, stop:0 rgba(49, 147, 250, 255), stop:1 rgba(34, 142, 255, 255));
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
max-height: 20px;
|
||||
border: 1px transparent grey;
|
||||
margin: 0px 20px 0px 20px;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
max-width: 20px;
|
||||
border: 1px transparent grey;
|
||||
margin: 20px 0px 20px 0px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
border-radius: 7px;
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(147, 200, 200);
|
||||
border-radius: 7px;
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
border-radius: 7px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(147, 200, 200);
|
||||
border-radius: 7px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::add-line:horizontal {
|
||||
border: 2px transparent grey;
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgba(34, 142, 255, 255);
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:horizontal:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgb(181,181,181);
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical {
|
||||
border: 2px transparent grey;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgba(34, 142, 255, 255);
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgb(181,181,181);
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgba(34, 142, 255, 255);
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgb(181,181,181);
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
background: rgba(34, 142, 255, 255);
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
background: rgb(181,181,181);
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::left-arrow:horizontal {
|
||||
border: 1px transparent grey;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: white;
|
||||
}
|
||||
QScrollBar::right-arrow:horizontal {
|
||||
border: 1px transparent grey;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: white;
|
||||
}
|
||||
QScrollBar::up-arrow:vertical {
|
||||
border: 1px transparent grey;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: white;
|
||||
}
|
||||
QScrollBar::down-arrow:vertical {
|
||||
border: 1px transparent grey;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: white;
|
||||
}
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
Dark Console Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 24/05/2018, 17:12.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/ConsoleStyle.qss
|
||||
*/
|
||||
QWidget {
|
||||
background-color:rgb(0, 0, 0);
|
||||
color: rgb(240, 240, 240);
|
||||
border-color: rgb(58, 58, 58);
|
||||
}
|
||||
|
||||
QPlainTextEdit {
|
||||
background-color:rgb(0, 0, 0);
|
||||
color: rgb(200, 200, 200);
|
||||
selection-background-color: rgb(255, 153, 0);
|
||||
selection-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
QTabWidget::pane {
|
||||
border-top: 1px solid #000000;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background-color:rgb(0, 0, 0);
|
||||
border-style: outset;
|
||||
border-width: 1px;
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-top-width: 0px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 153, 0);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected, QTabBar::tab:hover {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color:rgb(0, 0, 0);
|
||||
border-color:rgb(42, 42, 42);
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
border-bottom-right-radius:4px;
|
||||
border-bottom-left-radius:4px;
|
||||
}
|
||||
|
||||
QTabBar::tab:last:selected {
|
||||
background-color:rgb(0, 0, 0);
|
||||
border-color:rgb(42, 42, 42);
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
border-bottom-right-radius:4px;
|
||||
border-bottom-left-radius:4px;
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected {
|
||||
margin-bottom: 4px;
|
||||
border-bottom-right-radius:4px;
|
||||
border-bottom-left-radius:4px;
|
||||
}
|
||||
|
||||
QPushButton{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 6px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
||||
}
|
||||
|
||||
QPushButton:hover{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||
border-bottom-color: rgb(115, 115, 115);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 6px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(107, 107, 107, 255), stop:1 rgba(157, 157, 157, 255));
|
||||
}
|
||||
|
||||
QPushButton:pressed{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 6px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
||||
}
|
||||
|
||||
QPushButton:disabled{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(0, 0, 0);
|
||||
padding: 6px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(57, 57, 57, 255), stop:1 rgba(77, 77, 77, 255));
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
border-width: 1px; border-radius: 4px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
padding: 0 8px;
|
||||
color: rgb(255, 255, 255);
|
||||
background:rgb(101, 101, 101);
|
||||
selection-background-color: rgb(187, 187, 187);
|
||||
selection-color: rgb(60, 63, 65);
|
||||
}
|
||||
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: rgb(255, 255, 255);
|
||||
border-width: 1px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0.7, x2:0.5, y2:0.3, stop:0 rgba(0, 200, 0, 255), stop:1 rgba(30, 230, 30, 255));
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QMenuBar {
|
||||
background:rgb(0, 0, 0);
|
||||
color: rgb(255, 153, 0);
|
||||
}
|
||||
|
||||
QMenuBar::item {
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background:rgb(115, 115, 115);
|
||||
}
|
||||
|
||||
QMenu {
|
||||
border-width: 2px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(255, 153, 0);
|
||||
border-style: outset;
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
spacing: 3px;
|
||||
padding: 3px 15px;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
spacing: 3px;
|
||||
padding: 3px 15px;
|
||||
background:rgb(115, 115, 115);
|
||||
color:rgb(255, 255, 255);
|
||||
border-width: 1px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
ElegantDark Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 17/04/2018
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/ElegantDark.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:rgb(82, 82, 82);
|
||||
}
|
||||
QTextEdit {
|
||||
background-color:rgb(42, 42, 42);
|
||||
color: rgb(0, 255, 0);
|
||||
}
|
||||
QPushButton{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
||||
}
|
||||
QPushButton:hover{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||
border-bottom-color: rgb(115, 115, 115);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(107, 107, 107, 255), stop:1 rgba(157, 157, 157, 255));
|
||||
}
|
||||
QPushButton:pressed{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
||||
}
|
||||
QPushButton:disabled{
|
||||
border-style: outset;
|
||||
border-width: 2px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: rgb(0, 0, 0);
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(57, 57, 57, 255), stop:1 rgba(77, 77, 77, 255));
|
||||
}
|
||||
QLineEdit {
|
||||
border-width: 1px; border-radius: 4px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
padding: 0 8px;
|
||||
color: rgb(255, 255, 255);
|
||||
background:rgb(100, 100, 100);
|
||||
selection-background-color: rgb(187, 187, 187);
|
||||
selection-color: rgb(60, 63, 65);
|
||||
}
|
||||
QLabel {
|
||||
color:rgb(255,255,255);
|
||||
}
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: rgb(240, 240, 240);
|
||||
border-width: 1px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
background-color:rgb(77,77,77);
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0.7, x2:0.5, y2:0.3, stop:0 rgba(87, 97, 106, 255), stop:1 rgba(93, 103, 113, 255));
|
||||
border-radius: 5px;
|
||||
}
|
||||
QMenuBar {
|
||||
background:rgb(82, 82, 82);
|
||||
}
|
||||
QMenuBar::item {
|
||||
color:rgb(223,219,210);
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background:rgb(115, 115, 115);
|
||||
}
|
||||
QMenu::item:selected {
|
||||
color:rgb(255,255,255);
|
||||
border-width:2px;
|
||||
border-style:solid;
|
||||
padding-left:18px;
|
||||
padding-right:8px;
|
||||
padding-top:2px;
|
||||
padding-bottom:3px;
|
||||
background:qlineargradient(spread:pad, x1:0.5, y1:0.7, x2:0.5, y2:0.3, stop:0 rgba(87, 97, 106, 255), stop:1 rgba(93, 103, 113, 255));
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||
border-bottom-color: rgb(58, 58, 58);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
QMenu::item {
|
||||
color:rgb(223,219,210);
|
||||
background-color:rgb(78,78,78);
|
||||
padding-left:20px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:10px;
|
||||
}
|
||||
QMenu{
|
||||
background-color:rgb(78,78,78);
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:rgb(247,246,246);
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: rgb(77,77,77);
|
||||
background-color:rgb(101,101,101);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
padding:2px;
|
||||
color:rgb(250,250,250);
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-top-right-radius:4px;
|
||||
border-top-left-radius:4px;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(95, 92, 93, 255));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(95, 92, 93, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(95, 92, 93, 255));
|
||||
border-bottom-color: rgb(101,101,101);
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
background-color:rgb(101,101,101);
|
||||
margin-left: 0px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
QTabBar::tab:!selected {
|
||||
margin-top: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
QCheckBox {
|
||||
color:rgb(223,219,210);
|
||||
padding: 2px;
|
||||
}
|
||||
QCheckBox:hover {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
border-width:1px;
|
||||
border-color: rgb(87, 97, 106);
|
||||
background-color:qlineargradient(spread:pad, x1:0.5, y1:0.7, x2:0.5, y2:0.3, stop:0 rgba(87, 97, 106, 150), stop:1 rgba(93, 103, 113, 150));
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
border-width:1px;
|
||||
border-color: rgb(180,180,180);
|
||||
background-color:qlineargradient(spread:pad, x1:0.5, y1:0.7, x2:0.5, y2:0.3, stop:0 rgba(87, 97, 106, 255), stop:1 rgba(93, 103, 113, 255));
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
border-width:1px;
|
||||
border-color: rgb(87, 97, 106);
|
||||
background-color:rgb(255,255,255);
|
||||
}
|
||||
QStatusBar {
|
||||
color:rgb(240,240,240);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Jaime Quiroga
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* MacOS Style Sheet for QT Applications
|
||||
* Author: Jaime A. Quiroga P.
|
||||
* Company: GTRONICK
|
||||
* Last updated: 25/12/2020, 23:10.
|
||||
* Available at: https://github.com/GTRONICK/QSS/blob/master/MacOS.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:#ececec;
|
||||
}
|
||||
QPushButton, QToolButton, QCommandLinkButton{
|
||||
padding: 0 5px 0 5px;
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-right-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
color: #616161;
|
||||
font-weight: bold;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #fbfdfd, stop:0.5 #ffffff, stop:1 #fbfdfd);
|
||||
}
|
||||
QPushButton::default, QToolButton::default, QCommandLinkButton::default{
|
||||
border: 2px solid transparent;
|
||||
color: #FFFFFF;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #84afe5, stop:1 #1168e4);
|
||||
}
|
||||
QPushButton:hover, QToolButton:hover, QCommandLinkButton:hover{
|
||||
color: #3d3d3d;
|
||||
}
|
||||
QPushButton:pressed, QToolButton:pressed, QCommandLinkButton:pressed{
|
||||
color: #aeaeae;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #ffffff, stop:0.5 #fbfdfd, stop:1 #ffffff);
|
||||
}
|
||||
QPushButton:disabled, QToolButton:disabled, QCommandLinkButton:disabled{
|
||||
color: #616161;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #dce7eb, stop:0.5 #e0e8eb, stop:1 #dee7ec);
|
||||
}
|
||||
QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QDoubleSpinBox, QTimeEdit, QDateEdit, QDateTimeEdit {
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
background-color: #f4f4f4;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QTimeEdit:focus, QDateEdit:focus, QDateTimeEdit:focus {
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 #85b7e3, stop:1 #9ec1db);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #85b7e3, stop:1 #9ec1db);
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #85b7e3, stop:1 #9ec1db);
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 #85b7e3, stop:1 #9ec1db);
|
||||
background-color: #f4f4f4;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QTimeEdit:disabled, QDateEdit:disabled, QDateTimeEdit:disabled {
|
||||
color: #b9b9b9;
|
||||
}
|
||||
QSpinBox::up-button, QDoubleSpinBox::up-button, QTimeEdit::up-button, QDateEdit::up-button, QDateTimeEdit::up-button {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 15px;
|
||||
color: #272727;
|
||||
border-left-width: 1px;
|
||||
border-left-color: darkgray;
|
||||
border-left-style: solid;
|
||||
border-top-right-radius: 3px;
|
||||
padding: 3px;
|
||||
}
|
||||
QSpinBox::down-button, QDoubleSpinBox::down-button, QTimeEdit::down-button, QDateEdit::down-button, QDateTimeEdit::down-button {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: bottom right;
|
||||
width: 15px;
|
||||
color: #272727;
|
||||
border-left-width: 1px;
|
||||
border-left-color: darkgray;
|
||||
border-left-style: solid;
|
||||
border-bottom-right-radius: 3px;
|
||||
padding: 3px;
|
||||
}
|
||||
QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed, QTimeEdit::up-button:pressed, QDateEdit::up-button:pressed, QDateTimeEdit::up-button:pressed {
|
||||
color: #aeaeae;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #ffffff, stop:0.5 #fbfdfd, stop:1 #ffffff);
|
||||
}
|
||||
QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed, QTimeEdit::down-button:pressed, QDateEdit::down-button:pressed, QDateTimeEdit::down-button:pressed {
|
||||
color: #aeaeae;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #ffffff, stop:0.5 #fbfdfd, stop:1 #ffffff);
|
||||
}
|
||||
QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover, QTimeEdit::up-button:hover, QDateEdit::up-button:hover, QDateTimeEdit::up-button:hover {
|
||||
color: #FFFFFF;
|
||||
border-top-right-radius: 5px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #84afe5, stop:1 #1168e4);
|
||||
|
||||
}
|
||||
QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover, QTimeEdit::down-button:hover, QDateEdit::down-button:hover, QDateTimeEdit::down-button:hover {
|
||||
color: #FFFFFF;
|
||||
border-bottom-right-radius: 5px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #84afe5, stop:1 #1168e4);
|
||||
}
|
||||
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow, QTimeEdit::up-arrow, QDateEdit::up-arrow, QDateTimeEdit::up-arrow {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-up-symbolic.symbolic.png);
|
||||
}
|
||||
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow, QTimeEdit::down-arrow, QDateEdit::down-arrow, QDateTimeEdit::down-arrow {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-down-symbolic.symbolic.png);
|
||||
}
|
||||
QProgressBar {
|
||||
max-height: 8px;
|
||||
text-align: center;
|
||||
font: italic bold 11px;
|
||||
color: #3d3d3d;
|
||||
border: 1px solid transparent;
|
||||
border-radius:4px;
|
||||
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #ddd5d5, stop:0.5 #dad3d3, stop:1 #ddd5d5);
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #467dd1, stop:0.5 #3b88fc, stop:1 #467dd1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
QProgressBar:disabled {
|
||||
color: #616161;
|
||||
}
|
||||
QProgressBar::chunk:disabled {
|
||||
background-color: #aeaeae;
|
||||
}
|
||||
QSlider::groove {
|
||||
border: 1px solid #bbbbbb;
|
||||
background-color: #52595d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 6px;
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 6px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: #ffffff;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
width: 12px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: #ffffff;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
height: 12px;
|
||||
margin: 0 -5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::add-page, QSlider::sub-page {
|
||||
border: 1px transparent;
|
||||
background-color: #52595d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QSlider::add-page:horizontal {
|
||||
background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #ddd5d5, stop:0.5 #dad3d3, stop:1 #ddd5d5);
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #467dd1, stop:0.5 #3b88fc, stop:1 #467dd1);
|
||||
}
|
||||
QSlider::add-page:vertical {
|
||||
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #467dd1, stop:0.5 #3b88fc, stop:1 #467dd1);
|
||||
}
|
||||
QSlider::sub-page:vertical {
|
||||
background: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #ddd5d5, stop:0.5 #dad3d3, stop:1 #ddd5d5);
|
||||
}
|
||||
QSlider::add-page:horizontal:disabled, QSlider::sub-page:horizontal:disabled, QSlider::add-page:vertical:disabled, QSlider::sub-page:vertical:disabled {
|
||||
background: #b9b9b9;
|
||||
}
|
||||
QComboBox, QFontComboBox {
|
||||
border-width: 2px;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 #c1c9cf, stop:1 #d2d8dd);
|
||||
background-color: #f4f4f4;
|
||||
color: #272727;
|
||||
padding-left: 5px;
|
||||
}
|
||||
QComboBox:editable, QComboBox:!editable, QComboBox::drop-down:editable, QComboBox:!editable:on, QComboBox::drop-down:editable:on {
|
||||
background: #ffffff;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 15px;
|
||||
color: #272727;
|
||||
border-left-width: 1px;
|
||||
border-left-color: darkgray;
|
||||
border-left-style: solid;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-down-symbolic.symbolic.png); /*Adawaita icon thene*/
|
||||
}
|
||||
|
||||
QComboBox::down-arrow:on {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
border: 1px solid darkgray;
|
||||
border-radius: 8px;
|
||||
selection-background-color: #dadada;
|
||||
selection-color: #272727;
|
||||
color: #272727;
|
||||
background: white;
|
||||
}
|
||||
QLabel, QCheckBox, QRadioButton {
|
||||
color: #272727;
|
||||
}
|
||||
QCheckBox {
|
||||
padding: 2px;
|
||||
}
|
||||
QCheckBox:disabled, QRadioButton:disabled {
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
QCheckBox:hover {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
border-width:1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/object-select-symbolic.symbolic.png);
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #48a5fd;
|
||||
color: #ffffff;
|
||||
border-radius: 3px;
|
||||
background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #48a5fd, stop:0.5 #329cfb, stop:1 #48a5fd);
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #48a5fd;
|
||||
border-radius: 3px;
|
||||
background-color: #fbfdfa;
|
||||
}
|
||||
QLCDNumber {
|
||||
color: #616161;;
|
||||
}
|
||||
QMenuBar {
|
||||
background-color: #ececec;
|
||||
}
|
||||
QMenuBar::item {
|
||||
color: #616161;
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background-color: #ececec;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background-color: #dadada;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
QMenu {
|
||||
background-color: #ececec;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #dadada;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
QMenu::item {
|
||||
color: #616161;;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:#000000;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: #050a0e;
|
||||
background-color: #e0e0e0;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: -0.5em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
QTabWidget::tab-bar {
|
||||
alignment: center;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
border-bottom: 1px solid #c0c0c0;
|
||||
padding: 3px;
|
||||
color: #272727;
|
||||
background-color: #fefefc;
|
||||
margin-left:0px;
|
||||
}
|
||||
QTabBar::tab:!last {
|
||||
border-right: 1px solid;
|
||||
border-right-color: #c0c0c0;
|
||||
border-bottom-color: #c0c0c0;
|
||||
}
|
||||
QTabBar::tab:first {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
QTabBar::tab:last {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
color: #FFFFFF;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 #84afe5, stop:1 #1168e4);
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-style:solid;
|
||||
border-radius:7px;
|
||||
border-width: 1px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
border-color: #48a5fd;
|
||||
background-color: qradialgradient(cx:0.5, cy:0.5, radius:0.4,fx:0.5, fy:0.5, stop:0 #ffffff, stop:0.5 #ffffff, stop:0.6 #48a5fd, stop:1 #48a5fd);
|
||||
}
|
||||
QRadioButton::indicator:!checked {
|
||||
border-color: #a9b7c6;
|
||||
background-color: #fbfdfa;
|
||||
}
|
||||
QStatusBar {
|
||||
color:#027f7f;
|
||||
}
|
||||
|
||||
QDial {
|
||||
background: #16a085;
|
||||
}
|
||||
|
||||
QToolBox {
|
||||
color: #a9b7c6;
|
||||
background-color: #222b2e;
|
||||
}
|
||||
QToolBox::tab {
|
||||
color: #a9b7c6;
|
||||
background-color:#222b2e;
|
||||
}
|
||||
QToolBox::tab:selected {
|
||||
color: #FFFFFF;
|
||||
background-color:#222b2e;
|
||||
}
|
||||
QScrollArea {
|
||||
color: #FFFFFF;
|
||||
background-color:#222b2e;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
max-height: 10px;
|
||||
border: 1px transparent grey;
|
||||
margin: 0px 20px 0px 20px;
|
||||
background: transparent;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
max-width: 10px;
|
||||
border: 1px transparent grey;
|
||||
margin: 20px 0px 20px 0px;
|
||||
background: transparent;
|
||||
}
|
||||
QScrollBar::handle:vertical, QScrollBar::handle:horizontal {
|
||||
background: #52595d;
|
||||
border-style: transparent;
|
||||
border-radius: 4px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:hover {
|
||||
background: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #467dd1, stop:0.5 #3b88fc, stop:1 #467dd1);
|
||||
}
|
||||
QScrollBar::add-line, QScrollBar::sub-line {
|
||||
border: 2px transparent grey;
|
||||
border-radius: 4px;
|
||||
subcontrol-origin: margin;
|
||||
background: #b9b9b9;
|
||||
}
|
||||
QScrollBar::add-line:horizontal {
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
}
|
||||
QScrollBar::add-line:vertical {
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal {
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
}
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
}
|
||||
QScrollBar::add-line:vertical:pressed, QScrollBar::add-line:horizontal:pressed, QScrollBar::sub-line:horizontal:pressed, QScrollBar::sub-line:vertical:pressed {
|
||||
background: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #467dd1, stop:0.5 #3b88fc, stop:1 #467dd1);
|
||||
}
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::up-arrow:vertical {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-up-symbolic.symbolic.png);
|
||||
}
|
||||
QScrollBar::down-arrow:vertical {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-down-symbolic.symbolic.png);
|
||||
}
|
||||
QScrollBar::left-arrow:horizontal {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-previous-symbolic.symbolic.png);
|
||||
}
|
||||
QScrollBar::right-arrow:horizontal {
|
||||
image: url(/usr/share/icons/Adwaita/16x16/actions/go-next-symbolic.symbolic.png);
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
/*
|
||||
ManjaroMix Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 25/02/2020, 15:42.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/ManjaroMix.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:#151a1e;
|
||||
}
|
||||
QCalendar {
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QTextEdit {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
background-color: #222b2e;
|
||||
color: #d3dae3;
|
||||
}
|
||||
QPlainTextEdit {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
background-color: #222b2e;
|
||||
color: #d3dae3;
|
||||
}
|
||||
QToolButton {
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QToolButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(195, 195, 195), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(197, 197, 197), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(197, 197, 197));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(195, 195, 195), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(255,255,255);
|
||||
}
|
||||
QToolButton:pressed{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: rgb(0,0,0);
|
||||
padding: 2px;
|
||||
background-color: rgb(142,142,142);
|
||||
}
|
||||
QPushButton{
|
||||
border-style: solid;
|
||||
border-color: #050a0e;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QPushButton::default{
|
||||
border-style: solid;
|
||||
border-color: #050a0e;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #FFFFFF;
|
||||
padding: 2px;
|
||||
background-color: #151a1e;;
|
||||
}
|
||||
QPushButton:hover{
|
||||
border-style: solid;
|
||||
border-color: #050a0e;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
background-color: #1c1f1f;
|
||||
}
|
||||
QPushButton:pressed{
|
||||
border-style: solid;
|
||||
border-color: #050a0e;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
background-color: #2c2f2f;
|
||||
}
|
||||
QPushButton:disabled{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(217, 217, 217), stop:1 rgb(227, 227, 227));
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgb(227, 227, 227), stop:1 rgb(217, 217, 217));
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgb(215, 215, 215), stop:1 rgb(222, 222, 222));
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
background-color: rgb(142,142,142);
|
||||
}
|
||||
QLineEdit {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
background-color: #222b2e;
|
||||
color: #d3dae3;
|
||||
}
|
||||
QLabel {
|
||||
color: #d3dae3;
|
||||
}
|
||||
QLCDNumber {
|
||||
color: #4d9b87;
|
||||
}
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: #d3dae3;
|
||||
border-radius: 10px;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
background-color: #52595d;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #214037 ;
|
||||
border-radius: 10px;
|
||||
}
|
||||
QMenuBar {
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QMenuBar::item {
|
||||
color: #d3dae3;
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background-color: #252a2e;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
QMenu {
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #252a2e;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
QMenu::item {
|
||||
color: #d3dae3;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:#000000;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: #050a0e;
|
||||
background-color: #1e282c;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
QTabBar::tab:first {
|
||||
border-style: solid;
|
||||
border-left-width:1px;
|
||||
border-right-width:0px;
|
||||
border-top-width:1px;
|
||||
border-bottom-width:0px;
|
||||
border-top-color: #050a0e;
|
||||
border-left-color: #050a0e;
|
||||
border-bottom-color: #050a0e;
|
||||
border-top-left-radius: 4px;
|
||||
color: #d3dae3;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QTabBar::tab:last {
|
||||
border-style: solid;
|
||||
border-top-width:1px;
|
||||
border-left-width:1px;
|
||||
border-right-width:1px;
|
||||
border-bottom-width:0px;
|
||||
border-color: #050a0e;
|
||||
border-top-right-radius: 4px;
|
||||
color: #d3dae3;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QTabBar::tab {
|
||||
border-style: solid;
|
||||
border-top-width:1px;
|
||||
border-bottom-width:0px;
|
||||
border-left-width:1px;
|
||||
border-top-color: #050a0e;
|
||||
border-left-color: #050a0e;
|
||||
border-bottom-color: #050a0e;
|
||||
color: #d3dae3;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
border-style: solid;
|
||||
border-left-width:1px;
|
||||
border-bottom-width:0px;
|
||||
border-right-color: transparent;
|
||||
border-top-color: #050a0e;
|
||||
border-left-color: #050a0e;
|
||||
border-bottom-color: #050a0e;
|
||||
color: #FFFFFF;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: #1e282c;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected, QTabBar::tab:first:selected, QTabBar::tab:hover {
|
||||
border-style: solid;
|
||||
border-left-width:1px;
|
||||
border-bottom-width:0px;
|
||||
border-top-width:1px;
|
||||
border-right-color: transparent;
|
||||
border-top-color: #050a0e;
|
||||
border-left-color: #050a0e;
|
||||
border-bottom-color: #050a0e;
|
||||
color: #FFFFFF;
|
||||
padding: 3px;
|
||||
margin-left:0px;
|
||||
background-color: #1e282c;
|
||||
}
|
||||
|
||||
QCheckBox {
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
}
|
||||
QCheckBox:disabled {
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
QCheckBox:hover {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
border-width:1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #4fa08b;
|
||||
color: #000000;
|
||||
background-color: qradialgradient(cx:0.4, cy:0.4, radius: 1.5,fx:0, fy:0, stop:0 #1e282c, stop:0.3 #1e282c, stop:0.4 #4fa08b, stop:0.5 #1e282c, stop:1 #1e282c);
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #4fa08b;
|
||||
color: #000000;
|
||||
}
|
||||
QRadioButton {
|
||||
color: #d3dae3;
|
||||
padding: 1px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: #4fa08b;
|
||||
color: #a9b7c6;
|
||||
background-color: qradialgradient(cx:0.5, cy:0.5, radius:0.4,fx:0.5, fy:0.5, stop:0 #4fa08b, stop:1 #1e282c);
|
||||
}
|
||||
QRadioButton::indicator:!checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: #4fa08b;
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QStatusBar {
|
||||
color:#027f7f;
|
||||
}
|
||||
QSpinBox {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
QDoubleSpinBox {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
QTimeEdit {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
QDateTimeEdit {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
QDateEdit {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
QFontComboBox {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
QComboBox {
|
||||
color: #d3dae3;
|
||||
background-color: #222b2e;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #4fa08b;
|
||||
}
|
||||
|
||||
QDial {
|
||||
background: #16a085;
|
||||
}
|
||||
|
||||
QToolBox {
|
||||
color: #a9b7c6;
|
||||
background-color: #222b2e;
|
||||
}
|
||||
QToolBox::tab {
|
||||
color: #a9b7c6;
|
||||
background-color:#222b2e;
|
||||
}
|
||||
QToolBox::tab:selected {
|
||||
color: #FFFFFF;
|
||||
background-color:#222b2e;
|
||||
}
|
||||
QScrollArea {
|
||||
color: #FFFFFF;
|
||||
background-color:#222b2e;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 5px;
|
||||
background-color: #52595d;
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 5px;
|
||||
background-color: #52595d;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: #1a2224;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
width: 12px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: #1a2224;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
height: 12px;
|
||||
margin: 0 -5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::add-page:horizontal {
|
||||
background: #52595d;
|
||||
}
|
||||
QSlider::add-page:vertical {
|
||||
background: #52595d;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background-color: #15433a;
|
||||
}
|
||||
QSlider::sub-page:vertical {
|
||||
background-color: #15433a;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
max-height: 10px;
|
||||
border: 1px transparent grey;
|
||||
margin: 0px 20px 0px 20px;
|
||||
background: transparent;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
max-width: 10px;
|
||||
border: 1px transparent grey;
|
||||
margin: 20px 0px 20px 0px;
|
||||
background: transparent;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #52595d;
|
||||
border-style: transparent;
|
||||
border-radius: 4px;
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: #58a492;
|
||||
border-style: transparent;
|
||||
border-radius: 4px;
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: #52595d;
|
||||
border-style: transparent;
|
||||
border-radius: 4px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #58a492;
|
||||
border-style: transparent;
|
||||
border-radius: 4px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::add-line:horizontal {
|
||||
border: 2px transparent grey;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background: #15433a;
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:horizontal:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background: rgb(181,181,181);
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical {
|
||||
border: 2px transparent grey;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background: #15433a;
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background: rgb(181,181,181);
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background: #15433a;
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background: rgb(181,181,181);
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
background: #15433a;
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical:pressed {
|
||||
border: 2px transparent grey;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
background: rgb(181,181,181);
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
Material Dark Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Inspired on https://github.com/jxfwinter/qt-material-stylesheet
|
||||
Company: GTRONICK
|
||||
Last updated: 04/12/2018, 15:00.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/MaterialDark.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QDialog {
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QColorDialog {
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color:#1e1d23;
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QPlainTextEdit {
|
||||
selection-background-color:#007b50;
|
||||
background-color:#1e1d23;
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-width: 1px;
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QPushButton{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QPushButton::default{
|
||||
border-style: inset;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #04b97f;
|
||||
border-width: 1px;
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QToolButton {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #04b97f;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QToolButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #37efba;
|
||||
border-bottom-width: 2px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-bottom: 1px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QPushButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #37efba;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-bottom: 2px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QPushButton:pressed{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #37efba;
|
||||
border-bottom-width: 2px;
|
||||
border-style: solid;
|
||||
color: #37efba;
|
||||
padding-bottom: 1px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QPushButton:disabled{
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #808086;
|
||||
border-bottom-width: 2px;
|
||||
border-style: solid;
|
||||
color: #808086;
|
||||
padding-bottom: 1px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QLineEdit {
|
||||
border-width: 1px; border-radius: 4px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
padding: 0 8px;
|
||||
color: #a9b7c6;
|
||||
background:#1e1d23;
|
||||
selection-background-color:#007b50;
|
||||
selection-color: #FFFFFF;
|
||||
}
|
||||
QLabel {
|
||||
color: #a9b7c6;
|
||||
}
|
||||
QLCDNumber {
|
||||
color: #37e6b4;
|
||||
}
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: rgb(240, 240, 240);
|
||||
border-width: 1px;
|
||||
border-radius: 10px;
|
||||
border-color: rgb(58, 58, 58);
|
||||
border-style: inset;
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #04b97f;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QMenuBar {
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QMenuBar::item {
|
||||
color: #a9b7c6;
|
||||
spacing: 3px;
|
||||
padding: 1px 4px;
|
||||
background: #1e1d23;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background:#1e1d23;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: #04b97f;
|
||||
border-bottom-color: transparent;
|
||||
border-left-width: 2px;
|
||||
color: #FFFFFF;
|
||||
padding-left:15px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:7px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QMenu::item {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #a9b7c6;
|
||||
padding-left:17px;
|
||||
padding-top:4px;
|
||||
padding-bottom:4px;
|
||||
padding-right:7px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QMenu{
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: rgb(77,77,77);
|
||||
background-color:#1e1d23;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-width: 1px;
|
||||
border-style: solid;
|
||||
color: #808086;
|
||||
padding: 3px;
|
||||
margin-left:3px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
border-style: solid;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: #04b97f;
|
||||
border-bottom-width: 2px;
|
||||
border-style: solid;
|
||||
color: #FFFFFF;
|
||||
padding-left: 3px;
|
||||
padding-bottom: 2px;
|
||||
margin-left:3px;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
|
||||
QCheckBox {
|
||||
color: #a9b7c6;
|
||||
padding: 2px;
|
||||
}
|
||||
QCheckBox:disabled {
|
||||
color: #808086;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
QCheckBox:hover {
|
||||
border-radius:4px;
|
||||
border-style:solid;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
border-width:1px;
|
||||
border-color: rgb(87, 97, 106);
|
||||
background-color:#1e1d23;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #04b97f;
|
||||
color: #a9b7c6;
|
||||
background-color: #04b97f;
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
border-color: #04b97f;
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QRadioButton {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
padding: 1px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: #04b97f;
|
||||
color: #a9b7c6;
|
||||
background-color: #04b97f;
|
||||
}
|
||||
QRadioButton::indicator:!checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: #04b97f;
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QStatusBar {
|
||||
color:#027f7f;
|
||||
}
|
||||
QSpinBox {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QDoubleSpinBox {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QTimeEdit {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QDateTimeEdit {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QDateEdit {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QComboBox {
|
||||
color: #a9b7c6;
|
||||
background: #1e1d23;
|
||||
}
|
||||
QComboBox:editable {
|
||||
background: #1e1d23;
|
||||
color: #a9b7c6;
|
||||
selection-background-color: #1e1d23;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
color: #a9b7c6;
|
||||
background: #1e1d23;
|
||||
selection-color: #FFFFFF;
|
||||
selection-background-color: #1e1d23;
|
||||
}
|
||||
QComboBox:!editable:on, QComboBox::drop-down:editable:on {
|
||||
color: #a9b7c6;
|
||||
background: #1e1d23;
|
||||
}
|
||||
QFontComboBox {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QToolBox {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QToolBox::tab {
|
||||
color: #a9b7c6;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QToolBox::tab:selected {
|
||||
color: #FFFFFF;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QScrollArea {
|
||||
color: #FFFFFF;
|
||||
background-color: #1e1d23;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 5px;
|
||||
background: #04b97f;
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 5px;
|
||||
background: #04b97f;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
|
||||
border: 1px solid #5c5c5c;
|
||||
width: 14px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: qlineargradient(x1:1, y1:1, x2:0, y2:0, stop:0 #b4b4b4, stop:1 #8f8f8f);
|
||||
border: 1px solid #5c5c5c;
|
||||
height: 14px;
|
||||
margin: 0 -5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::add-page:horizontal {
|
||||
background: white;
|
||||
}
|
||||
QSlider::add-page:vertical {
|
||||
background: white;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #04b97f;
|
||||
}
|
||||
QSlider::sub-page:vertical {
|
||||
background: #04b97f;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Neon Style Sheet for QT Applications (QpushButton)
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 24/10/2020, 15:42.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/NeonButtons.qss
|
||||
*/
|
||||
QPushButton{
|
||||
border-style: solid;
|
||||
border-color: #050a0e;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
background-color: #100E19;
|
||||
}
|
||||
QPushButton::default{
|
||||
border-style: solid;
|
||||
border-color: #050a0e;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #FFFFFF;
|
||||
padding: 2px;
|
||||
background-color: #151a1e;
|
||||
}
|
||||
QPushButton:hover{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:1, stop:0 #C0DB50, stop:0.4 #C0DB50, stop:0.5 #100E19, stop:1 #100E19);
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:1, stop:0 #100E19, stop:0.5 #100E19, stop:0.6 #C0DB50, stop:1 #C0DB50);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #C0DB50, stop:0.3 #C0DB50, stop:0.7 #100E19, stop:1 #100E19);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 #C0DB50, stop:0.3 #C0DB50, stop:0.7 #100E19, stop:1 #100E19);
|
||||
border-width: 2px;
|
||||
border-radius: 1px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
}
|
||||
QPushButton:pressed{
|
||||
border-style: solid;
|
||||
border-top-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:1, stop:0 #d33af1, stop:0.4 #d33af1, stop:0.5 #100E19, stop:1 #100E19);
|
||||
border-bottom-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:1, stop:0 #100E19, stop:0.5 #100E19, stop:0.6 #d33af1, stop:1 #d33af1);
|
||||
border-left-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #d33af1, stop:0.3 #d33af1, stop:0.7 #100E19, stop:1 #100E19);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 #d33af1, stop:0.3 #d33af1, stop:0.7 #100E19, stop:1 #100E19);
|
||||
border-width: 2px;
|
||||
border-radius: 1px;
|
||||
color: #d3dae3;
|
||||
padding: 2px;
|
||||
}
|
||||
|
After Width: | Height: | Size: 166 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 147 B |
|
After Width: | Height: | Size: 165 B |
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1,45 @@
|
||||
### QT StyleSheet templates ###
|
||||
Themes available:
|
||||
1. [Ubuntu](https://github.com/GTRONICK/QSS/blob/master/Ubuntu.qss)
|
||||
|
||||

|
||||
|
||||
2. [ElegantDark](https://github.com/GTRONICK/QSS/blob/master/ElegantDark.qss)
|
||||
|
||||

|
||||
|
||||
3. [MaterialDark](https://github.com/GTRONICK/QSS/blob/master/MaterialDark.qss)
|
||||
|
||||

|
||||
|
||||
4. [ConsoleStyle](https://github.com/GTRONICK/QSS/blob/master/ConsoleStyle.qss)
|
||||
|
||||

|
||||
|
||||
5. [AMOLED](https://github.com/GTRONICK/QSS/blob/master/AMOLED.qss)
|
||||
|
||||

|
||||
|
||||
6. [Aqua](https://github.com/GTRONICK/QSS/blob/master/Aqua.qss)
|
||||
|
||||

|
||||
|
||||
## The ManjaroMix Theme!: Includes a radial gradient for Checkboxes, and minimalist arrows for scrollbars. ##
|
||||
7. [ManjaroMix](https://github.com/GTRONICK/QSS/blob/master/ManjaroMix.qss)
|
||||
|
||||

|
||||
|
||||
8. [NeonButtons](https://github.com/GTRONICK/QSS/blob/master/NeonButtons.qss)
|
||||
|
||||

|
||||

|
||||
|
||||
## MacOS Theme!: Reduced code, image integration through URL resources. ##
|
||||
9. [MacOS](https://github.com/GTRONICK/QSS/blob/master/MacOS.qss)
|
||||
|
||||

|
||||
**Added images in QSS_IMG folder**
|
||||
|
||||
Stay tunned!, this files are being updated frequently.
|
||||
*Consider donating :)* **PayPal Account:** gtronick@gmail.com
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
/*
|
||||
Ubuntu Style Sheet for QT Applications
|
||||
Author: Jaime A. Quiroga P.
|
||||
Company: GTRONICK
|
||||
Last updated: 01/10/2021 (dd/mm/yyyy), 15:18.
|
||||
Available at: https://github.com/GTRONICK/QSS/blob/master/Ubuntu.qss
|
||||
*/
|
||||
QMainWindow {
|
||||
background-color:#f0f0f0;
|
||||
}
|
||||
QCheckBox {
|
||||
padding:2px;
|
||||
}
|
||||
QCheckBox:hover {
|
||||
border:1px solid rgb(255,150,60);
|
||||
border-radius:4px;
|
||||
padding: 1px;
|
||||
background-color:qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(190, 90, 50, 50), stop:1 rgba(250, 130, 40, 50));
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border:1px solid rgb(246, 134, 86);
|
||||
border-radius:4px;
|
||||
background-color:rgb(246, 134, 86)
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border-width:1px solid rgb(246, 134, 86);
|
||||
border-radius:4px;
|
||||
background-color:rgb(255,255,255);
|
||||
}
|
||||
QColorDialog {
|
||||
background-color:#f0f0f0;
|
||||
}
|
||||
QComboBox {
|
||||
color:rgb(81,72,65);
|
||||
background: #ffffff;
|
||||
}
|
||||
QComboBox:editable {
|
||||
selection-color:rgb(81,72,65);
|
||||
selection-background-color: #ffffff;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
selection-color: #ffffff;
|
||||
selection-background-color: rgb(246, 134, 86);
|
||||
}
|
||||
QComboBox:!editable:on, QComboBox::drop-down:editable:on {
|
||||
color: #1e1d23;
|
||||
}
|
||||
QDateTimeEdit, QDateEdit, QDoubleSpinBox, QFontComboBox {
|
||||
color:rgb(81,72,65);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
QDialog {
|
||||
background-color:#f0f0f0;
|
||||
}
|
||||
|
||||
QLabel,QLineEdit {
|
||||
color:rgb(17,17,17);
|
||||
}
|
||||
QLineEdit {
|
||||
background-color:rgb(255,255,255);
|
||||
selection-background-color:rgb(236,116,64);
|
||||
}
|
||||
QMenuBar {
|
||||
color:rgb(223,219,210);
|
||||
background-color:rgb(65,64,59);
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding-top:4px;
|
||||
padding-left:4px;
|
||||
padding-right:4px;
|
||||
color:rgb(223,219,210);
|
||||
background-color:rgb(65,64,59);
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
color:rgb(255,255,255);
|
||||
padding-top:2px;
|
||||
padding-left:2px;
|
||||
padding-right:2px;
|
||||
border-top-width:2px;
|
||||
border-left-width:2px;
|
||||
border-right-width:2px;
|
||||
border-top-right-radius:4px;
|
||||
border-top-left-radius:4px;
|
||||
border-style:solid;
|
||||
background-color:rgb(65,64,59);
|
||||
border-top-color: rgb(47,47,44);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(90, 87, 78, 255), stop:1 rgba(47,47,44, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 rgba(90, 87, 78, 255), stop:1 rgba(47,47,44, 255));
|
||||
}
|
||||
QMenu {
|
||||
color:rgb(223,219,210);
|
||||
background-color:rgb(65,64,59);
|
||||
}
|
||||
QMenu::item {
|
||||
color:rgb(223,219,210);
|
||||
padding:4px 10px 4px 20px;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
color:rgb(255,255,255);
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(225, 108, 54, 255), stop:1 rgba(246, 134, 86, 255));
|
||||
border-style:solid;
|
||||
border-width:3px;
|
||||
padding:4px 7px 4px 17px;
|
||||
border-bottom-color:qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(175,85,48,255), stop:1 rgba(236,114,67, 255));
|
||||
border-top-color:qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(253,156,113,255), stop:1 rgba(205,90,46, 255));
|
||||
border-right-color:qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgba(253,156,113,255), stop:1 rgba(205,90,46, 255));
|
||||
border-left-color:qlineargradient(spread:pad, x1:1, y1:0.5, x2:0, y2:0.5, stop:0 rgba(253,156,113,255), stop:1 rgba(205,90,46, 255));
|
||||
}
|
||||
QPlainTextEdit {
|
||||
border: 1px solid transparent;
|
||||
color:rgb(17,17,17);
|
||||
selection-background-color:rgb(236,116,64);
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
QProgressBar {
|
||||
text-align: center;
|
||||
color: rgb(0, 0, 0);
|
||||
border: 1px inset rgb(150,150,150);
|
||||
border-radius: 10px;
|
||||
background-color:rgb(221,221,219);
|
||||
}
|
||||
QProgressBar::chunk:horizontal {
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(225, 108, 54, 255), stop:1 rgba(246, 134, 86, 255));
|
||||
border:1px solid;
|
||||
border-radius:8px;
|
||||
border-bottom-color:qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(175,85,48,255), stop:1 rgba(236,114,67, 255));
|
||||
border-top-color:qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(253,156,113,255), stop:1 rgba(205,90,46, 255));
|
||||
border-right-color:qlineargradient(spread:pad, x1:0, y1:0.5, x2:1, y2:0.5, stop:0 rgba(253,156,113,255), stop:1 rgba(205,90,46, 255));
|
||||
border-left-color:qlineargradient(spread:pad, x1:1, y1:0.5, x2:0, y2:0.5, stop:0 rgba(253,156,113,255), stop:1 rgba(205,90,46, 255));
|
||||
}
|
||||
QPushButton{
|
||||
color:rgb(17,17,17);
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
border-bottom-color: rgb(150,150,150);
|
||||
border-right-color: rgb(165,165,165);
|
||||
border-left-color: rgb(165,165,165);
|
||||
border-top-color: rgb(180,180,180);
|
||||
border-style: solid;
|
||||
padding: 4px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(220, 220, 220, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QPushButton:hover{
|
||||
color:rgb(17,17,17);
|
||||
border-width: 1px;
|
||||
border-radius:6px;
|
||||
border-top-color: rgb(255,150,60);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(200, 70, 20, 255), stop:1 rgba(255,150,60, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 rgba(200, 70, 20, 255), stop:1 rgba(255,150,60, 255));
|
||||
border-bottom-color: rgb(200,70,20);
|
||||
border-style: solid;
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(220, 220, 220, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QPushButton:default{
|
||||
color:rgb(17,17,17);
|
||||
border-width: 1px;
|
||||
border-radius:6px;
|
||||
border-top-color: rgb(255,150,60);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(200, 70, 20, 255), stop:1 rgba(255,150,60, 255));
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 rgba(200, 70, 20, 255), stop:1 rgba(255,150,60, 255));
|
||||
border-bottom-color: rgb(200,70,20);
|
||||
border-style: solid;
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(220, 220, 220, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QPushButton:pressed{
|
||||
color:rgb(17,17,17);
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
border-top-color: rgba(255,150,60,200);
|
||||
border-right-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(200, 70, 20, 255), stop:1 rgba(255,150,60, 200));
|
||||
border-left-color: qlineargradient(spread:pad, x1:1, y1:0, x2:0, y2:0, stop:0 rgba(200, 70, 20, 255), stop:1 rgba(255,150,60, 200));
|
||||
border-bottom-color: rgba(200,70,20,200);
|
||||
border-style: solid;
|
||||
padding: 2px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:0, x2:0.5, y2:1, stop:0 rgba(220, 220, 220, 255), stop:1 rgba(255, 255, 255, 255));
|
||||
}
|
||||
QPushButton:disabled{
|
||||
color:rgb(174,167,159);
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(200, 200, 200, 255), stop:1 rgba(230, 230, 230, 255));
|
||||
}
|
||||
QRadioButton {
|
||||
padding: 1px;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(246, 134, 86, 255);
|
||||
color: #a9b7c6;
|
||||
background-color:rgba(246, 134, 86, 255);
|
||||
}
|
||||
QRadioButton::indicator:!checked {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-style:solid;
|
||||
border-radius:5px;
|
||||
border-width: 1px;
|
||||
border-color: rgb(246, 134, 86);
|
||||
color: #a9b7c6;
|
||||
background-color: transparent;
|
||||
}
|
||||
QScrollArea {
|
||||
color: white;
|
||||
background-color:#f0f0f0;
|
||||
}
|
||||
QSlider::groove {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 5px;
|
||||
background: rgb(246, 134, 86);
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 5px;
|
||||
background: rgb(246, 134, 86);
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
width: 12px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
background: rgb(253,253,253);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgb(207,207,207);
|
||||
height: 12px;
|
||||
margin: 0 -5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
QSlider::add-page:horizontal, QSlider::add-page:vertical {
|
||||
background: white;
|
||||
}
|
||||
QSlider::sub-page:horizontal, QSlider::sub-page:vertical {
|
||||
background: rgb(246, 134, 86);
|
||||
}
|
||||
QStatusBar, QSpinBox {
|
||||
color:rgb(81,72,65);
|
||||
}
|
||||
QSpinBox {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
max-height: 20px;
|
||||
border: 1px transparent;
|
||||
margin: 0px 20px 0px 20px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: rgb(253,253,253);
|
||||
border: 1px solid rgb(207,207,207);
|
||||
border-radius: 7px;
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: rgb(253,253,253);
|
||||
border: 1px solid rgb(255,150,60);
|
||||
border-radius: 7px;
|
||||
min-width: 25px;
|
||||
}
|
||||
QScrollBar::add-line:horizontal {
|
||||
border: 1px solid rgb(207,207,207);
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:horizontal:hover {
|
||||
border: 1px solid rgb(255,150,60);
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:horizontal:pressed {
|
||||
border: 1px solid grey;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background: rgb(231,231,231);
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal {
|
||||
border: 1px solid rgb(207,207,207);
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal:hover {
|
||||
border: 1px solid rgb(255,150,60);
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal:pressed {
|
||||
border: 1px solid grey;
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgb(231,231,231);
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::left-arrow:horizontal {
|
||||
border: 1px transparent grey;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(230,230,230);
|
||||
}
|
||||
QScrollBar::right-arrow:horizontal {
|
||||
border: 1px transparent grey;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(230,230,230);
|
||||
}
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
max-width: 20px;
|
||||
border: 1px transparent grey;
|
||||
margin: 20px 0px 20px 0px;
|
||||
}
|
||||
QScrollBar::add-line:vertical {
|
||||
border: 1px solid;
|
||||
border-color: rgb(207,207,207);
|
||||
border-bottom-right-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical:hover {
|
||||
border: 1px solid;
|
||||
border-color: rgb(255,150,60);
|
||||
border-bottom-right-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::add-line:vertical:pressed {
|
||||
border: 1px solid grey;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
background: rgb(231,231,231);
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical {
|
||||
border: 1px solid rgb(207,207,207);
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical:hover {
|
||||
border: 1px solid rgb(255,150,60);
|
||||
border-top-right-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
background: rgb(255, 255, 255);
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::sub-line:vertical:pressed {
|
||||
border: 1px solid grey;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
background: rgb(231,231,231);
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: rgb(253,253,253);
|
||||
border: 1px solid rgb(207,207,207);
|
||||
border-radius: 7px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: rgb(253,253,253);
|
||||
border: 1px solid rgb(255,150,60);
|
||||
border-radius: 7px;
|
||||
min-height: 25px;
|
||||
}
|
||||
QScrollBar::up-arrow:vertical {
|
||||
border: 1px transparent grey;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(230,230,230);
|
||||
}
|
||||
QScrollBar::down-arrow:vertical {
|
||||
border: 1px transparent grey;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgb(230,230,230);
|
||||
}
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
QTabWidget {
|
||||
color:rgb(0,0,0);
|
||||
background-color:rgb(247,246,246);
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border-color: rgb(180,180,180);
|
||||
background-color:rgb(247,246,246);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
padding-left:4px;
|
||||
padding-right:4px;
|
||||
padding-bottom:2px;
|
||||
padding-top:2px;
|
||||
color:rgb(81,72,65);
|
||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(221,218,217,255), stop:1 rgba(240,239,238,255));
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-top-right-radius:4px;
|
||||
border-top-left-radius:4px;
|
||||
border-top-color: rgb(180,180,180);
|
||||
border-left-color: rgb(180,180,180);
|
||||
border-right-color: rgb(180,180,180);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover {
|
||||
background-color:rgb(247,246,246);
|
||||
margin-left: 0px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
QTabBar::tab:!selected {
|
||||
margin-top: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
QTextEdit {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color:transparent;
|
||||
color:rgb(17,17,17);
|
||||
selection-background-color:rgb(236,116,64);
|
||||
}
|
||||
QTimeEdit, QToolBox, QToolBox::tab, QToolBox::tab:selected {
|
||||
color:rgb(81,72,65);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "ugeneral.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication a(argc, argv);
|
||||
uGeneral w;
|
||||
w.show();
|
||||
return a.exec();
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
// neuralnetworkmanager.cpp
|
||||
#include "neuralnetworkmanager.h"
|
||||
#include <QNetworkRequest>
|
||||
#include <QJsonDocument>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <QSslCipher>
|
||||
#include <QSslSocket>
|
||||
#include <QFile>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
NeuralNetworkManager::NeuralNetworkManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, networkManager(new QNetworkAccessManager(this))
|
||||
{
|
||||
|
||||
// Инициализируем SSL конфигурацию
|
||||
setupSSL();
|
||||
|
||||
// Инициализируем конфигурации сетей
|
||||
setupDefaultConfigs();
|
||||
|
||||
// Инициализируем GigaChat токен как невалидный
|
||||
gigaChatAuth.tokenExpiry = 0;
|
||||
|
||||
// Подключаем обработчик сетевых ответов
|
||||
connect(networkManager, &QNetworkAccessManager::finished,
|
||||
this, &NeuralNetworkManager::handleNetworkReply);
|
||||
|
||||
}
|
||||
|
||||
NeuralNetworkManager::~NeuralNetworkManager()
|
||||
{
|
||||
// Очищаем очередь сообщений
|
||||
pendingMessages.clear();
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setupSSL()
|
||||
{
|
||||
// Настройка стандартной SSL конфигурации
|
||||
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
||||
|
||||
// Устанавливаем протокол TLS 1.2 или новее
|
||||
sslConfig.setProtocol(QSsl::TlsV1_2OrLater);
|
||||
sslConfig.setPeerVerifyMode(QSslSocket::VerifyPeer);
|
||||
|
||||
// Загружаем российские корневые сертификаты если доступны
|
||||
QStringList certPaths = {
|
||||
"/etc/ssl/certs/russian_trusted_root_ca.pem",
|
||||
":/certs/russian_ca.pem",
|
||||
"./certs/russian_ca.pem"
|
||||
};
|
||||
|
||||
QList<QSslCertificate> russianCerts;
|
||||
for (const QString &path : certPaths) {
|
||||
if (QFile::exists(path)) {
|
||||
QFile certFile(path);
|
||||
if (certFile.open(QIODevice::ReadOnly)) {
|
||||
QByteArray certData = certFile.readAll();
|
||||
russianCerts += QSslCertificate::fromData(certData);
|
||||
certFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!russianCerts.isEmpty()) {
|
||||
QList<QSslCertificate> defaultCerts = sslConfig.caCertificates();
|
||||
defaultCerts += russianCerts;
|
||||
sslConfig.setCaCertificates(defaultCerts);
|
||||
}
|
||||
|
||||
// Устанавливаем как конфигурацию по умолчанию
|
||||
QSslConfiguration::setDefaultConfiguration(sslConfig);
|
||||
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setupSSLForGigaChat()
|
||||
{
|
||||
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
|
||||
|
||||
// Критически важные настройки для GigaChat
|
||||
config.setProtocol(QSsl::TlsV1_2OrLater);
|
||||
config.setPeerVerifyMode(QSslSocket::VerifyPeer);
|
||||
|
||||
// Дополнительные настройки для совместимости
|
||||
config.setSslOption(QSsl::SslOptionDisableSessionTickets, false);
|
||||
config.setSslOption(QSsl::SslOptionDisableCompression, false);
|
||||
config.setSslOption(QSsl::SslOptionDisableServerNameIndication, false);
|
||||
config.setPeerVerifyDepth(2);
|
||||
|
||||
QSslConfiguration::setDefaultConfiguration(config);
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setupDefaultConfigs()
|
||||
{
|
||||
// DeepSeek
|
||||
NetworkConfig deepseekConfig;
|
||||
deepseekConfig.baseUrl = "https://api.deepseek.com";
|
||||
deepseekConfig.endpoint = "/chat/completions";
|
||||
deepseekConfig.model = "deepseek-chat";
|
||||
networkConfigs[DeepSeek] = deepseekConfig;
|
||||
|
||||
// GigaChat
|
||||
NetworkConfig gigachatConfig;
|
||||
gigachatConfig.baseUrl = "https://gigachat.devices.sberbank.ru";
|
||||
gigachatConfig.endpoint = "/api/v1/chat/completions";
|
||||
gigachatConfig.model = "GigaChat";
|
||||
networkConfigs[GigaChat] = gigachatConfig;
|
||||
|
||||
// ChatGPT
|
||||
NetworkConfig chatgptConfig;
|
||||
chatgptConfig.baseUrl = "https://api.openai.com";
|
||||
chatgptConfig.endpoint = "/v1/chat/completions";
|
||||
chatgptConfig.model = "gpt-3.5-turbo";
|
||||
networkConfigs[ChatGPT] = chatgptConfig;
|
||||
|
||||
// Ollama
|
||||
NetworkConfig ollamaConfig;
|
||||
ollamaConfig.baseUrl = "http://localhost:11434";
|
||||
ollamaConfig.endpoint = "/api/chat";
|
||||
ollamaConfig.model = "zephyr:7b";
|
||||
networkConfigs[Ollama] = ollamaConfig;
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::sendMessage(const QString &message, NetworkType networkType)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
if (!networkConfigs.contains(networkType)) {
|
||||
emit errorOccurred("Неизвестный тип нейросети");
|
||||
return;
|
||||
}
|
||||
|
||||
// Для GigaChat проверяем токен
|
||||
if (networkType == GigaChat) {
|
||||
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
if (!isGigaChatTokenValid()) {
|
||||
|
||||
|
||||
// Добавляем сообщение в очередь
|
||||
PendingMessage pendingMsg;
|
||||
pendingMsg.message = message;
|
||||
pendingMsg.networkType = networkType;
|
||||
pendingMessages.append(pendingMsg);
|
||||
|
||||
// Запрашиваем новый токен если еще не запрашиваем
|
||||
if (!isProcessingGigaChatToken) {
|
||||
requestGigaChatToken();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Если токен валиден, отправляем сообщение сразу
|
||||
sendMessageInternal(message, networkType);
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::sendMessageInternal(const QString &message, NetworkType networkType)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString fullMessage = currentPrefix.isEmpty() ? message : currentPrefix + " " + message;
|
||||
|
||||
QNetworkRequest request = prepareRequest(networkType);
|
||||
QByteArray requestData = prepareRequestBody(networkType, fullMessage);
|
||||
|
||||
|
||||
|
||||
// Устанавливаем таймаут
|
||||
// request.setTransferTimeout(30000);
|
||||
|
||||
QNetworkReply *reply = networkManager->post(request, requestData);
|
||||
reply->setProperty("networkType", networkType);
|
||||
|
||||
// Для GigaChat обрабатываем SSL ошибки
|
||||
if (networkType == GigaChat) {
|
||||
connect(reply, &QNetworkReply::sslErrors,
|
||||
this, [reply](const QList<QSslError> &errors) {
|
||||
for (const QSslError &error : errors) {
|
||||
}
|
||||
reply->ignoreSslErrors();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setPrefix(const QString &prefix)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
currentPrefix = prefix;
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setApiKey(NetworkType networkType, const QString &apiKey)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
if (networkConfigs.contains(networkType)) {
|
||||
networkConfigs[networkType].apiKey = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setModel(NetworkType networkType, const QString &model)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
if (networkConfigs.contains(networkType)) {
|
||||
networkConfigs[networkType].model = model;
|
||||
}
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setBaseUrl(NetworkType networkType, const QString &url)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
if (networkConfigs.contains(networkType)) {
|
||||
networkConfigs[networkType].baseUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setOllamaUrl(const QString &url)
|
||||
{
|
||||
setBaseUrl(Ollama, url);
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::setGigaChatCredentials(const QString &clientId,
|
||||
const QString &authorizationCode)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
gigaChatAuth.clientId = clientId;
|
||||
gigaChatAuth.authorizationCode = authorizationCode;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::refreshGigaChatToken()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
if (!gigaChatAuth.clientId.isEmpty() && !gigaChatAuth.authorizationCode.isEmpty()) {
|
||||
requestGigaChatToken();
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkRequest NeuralNetworkManager::prepareRequest(NetworkType type)
|
||||
{
|
||||
if (!networkConfigs.contains(type)) {
|
||||
qWarning() << "Попытка подготовить запрос для неизвестного типа:" << type;
|
||||
return QNetworkRequest();
|
||||
}
|
||||
|
||||
NetworkConfig config = networkConfigs[type];
|
||||
|
||||
// Проверяем и корректируем URL
|
||||
QString baseUrl = config.baseUrl;
|
||||
QString endpoint = config.endpoint;
|
||||
|
||||
if (baseUrl.isEmpty()) {
|
||||
qWarning() << "Base URL пуст для типа" << type;
|
||||
return QNetworkRequest();
|
||||
}
|
||||
|
||||
// Убедимся, что baseUrl имеет схему
|
||||
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
QString urlStr = baseUrl + endpoint;
|
||||
QUrl url(urlStr);
|
||||
|
||||
if (!url.isValid()) {
|
||||
qWarning() << "Некорректный URL:" << urlStr;
|
||||
return QNetworkRequest();
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// ОСОБАЯ обработка для GigaChat
|
||||
if (type == GigaChat) {
|
||||
|
||||
|
||||
// SSL конфигурация для GigaChat
|
||||
setupSSLForGigaChat();
|
||||
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
||||
request.setSslConfiguration(sslConfig);
|
||||
|
||||
// Заголовки для GigaChat
|
||||
request.setRawHeader("Accept-Encoding", "gzip, deflate, br");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
request.setRawHeader("Content-Type", "application/json");
|
||||
|
||||
if (!gigaChatAuth.accessToken.isEmpty()) {
|
||||
QString authHeader = QString("Bearer %1").arg(gigaChatAuth.accessToken);
|
||||
request.setRawHeader("Authorization", authHeader.toUtf8());
|
||||
} else {
|
||||
qWarning() << "Токен GigaChat отсутствует!";
|
||||
}
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
// Общая настройка для других сервисов
|
||||
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
||||
sslConfig.setPeerVerifyMode(QSslSocket::VerifyPeer);
|
||||
request.setSslConfiguration(sslConfig);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
if (type == DeepSeek || type == ChatGPT) {
|
||||
if (!config.apiKey.isEmpty()) {
|
||||
request.setRawHeader("Authorization",
|
||||
QString("Bearer %1").arg(config.apiKey).toUtf8());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Общие заголовки
|
||||
request.setRawHeader("User-Agent", "TwitchBot/1.0 (Qt Network)");
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
QByteArray NeuralNetworkManager::prepareRequestBody(NetworkType type, const QString &message)
|
||||
{
|
||||
QJsonObject requestBody;
|
||||
QJsonArray messages;
|
||||
|
||||
QJsonObject messageObj;
|
||||
messageObj["role"] = "user";
|
||||
messageObj["content"] = message;
|
||||
messages.append(messageObj);
|
||||
|
||||
if (type == Ollama) {
|
||||
// Формат запроса для Ollama
|
||||
requestBody["model"] = networkConfigs[type].model;
|
||||
requestBody["messages"] = messages;
|
||||
requestBody["stream"] = false;
|
||||
|
||||
QJsonObject options;
|
||||
options["temperature"] = 0.7;
|
||||
options["num_predict"] = 200;
|
||||
requestBody["options"] = options;
|
||||
} else {
|
||||
// Формат запроса для остальных API (OpenAI-совместимый)
|
||||
requestBody["model"] = networkConfigs[type].model;
|
||||
requestBody["messages"] = messages;
|
||||
requestBody["max_tokens"] = 300;
|
||||
requestBody["temperature"] = 0.7;
|
||||
requestBody["stream"] = false;
|
||||
}
|
||||
|
||||
return QJsonDocument(requestBody).toJson();
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::handleNetworkReply(QNetworkReply *reply)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
// Определяем тип запроса
|
||||
bool isTokenRequest = reply->property("isTokenRequest").toBool();
|
||||
QString url = reply->url().toString();
|
||||
|
||||
|
||||
// Если это запрос токена GigaChat
|
||||
if (isTokenRequest || url.contains("oauth")) {
|
||||
handleGigaChatTokenReply(reply);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка обычных ответов от нейросетей
|
||||
NetworkType type = static_cast<NetworkType>(reply->property("networkType").toInt());
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMsg = QString("Ошибка сети (%1): %2")
|
||||
.arg(type)
|
||||
.arg(reply->errorString());
|
||||
|
||||
|
||||
// Для GigaChat проверяем, не истек ли токен
|
||||
if (type == GigaChat &&
|
||||
(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401 ||
|
||||
reply->error() == QNetworkReply::AuthenticationRequiredError)) {
|
||||
|
||||
gigaChatAuth.tokenExpiry = 0; // Помечаем токен как устаревший
|
||||
|
||||
// Запрашиваем новый токен
|
||||
if (!isProcessingGigaChatToken) {
|
||||
requestGigaChatToken();
|
||||
}
|
||||
} else {
|
||||
emit errorOccurred(errorMsg);
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QString response = parseResponse(type, data);
|
||||
|
||||
if (!response.isEmpty()) {
|
||||
QString truncatedResponse = truncateResponse(response);
|
||||
emit responseReceived(truncatedResponse);
|
||||
} else {
|
||||
emit errorOccurred("Не удалось получить ответ от нейросети");
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::handleGigaChatTokenReply(QNetworkReply *reply)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
|
||||
isProcessingGigaChatToken = false;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMsg = QString("Ошибка получения токена GigaChat: %1").arg(reply->errorString());
|
||||
|
||||
// Очищаем очередь сообщений если не удалось получить токен
|
||||
pendingMessages.clear();
|
||||
emit errorOccurred(errorMsg);
|
||||
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isNull()) {
|
||||
pendingMessages.clear();
|
||||
emit errorOccurred("Неверный формат ответа от GigaChat");
|
||||
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json = doc.object();
|
||||
|
||||
if (json.contains("access_token")) {
|
||||
gigaChatAuth.accessToken = json["access_token"].toString();
|
||||
|
||||
int expiresIn = json.contains("expires_in")
|
||||
? json["expires_in"].toInt(1800)
|
||||
: 1800;
|
||||
|
||||
// Устанавливаем время истечения (текущее время + expiresIn секунд)
|
||||
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
gigaChatAuth.tokenExpiry = currentTime + expiresIn - 300; // Вычитаем 5 минут для запаса
|
||||
|
||||
emit gigaChatTokenRefreshed();
|
||||
|
||||
// Обрабатываем сообщения из очереди
|
||||
processPendingMessages();
|
||||
|
||||
} else if (json.contains("error")) {
|
||||
QString errorMsg = json["error"].toString();
|
||||
|
||||
// Очищаем очередь сообщений
|
||||
pendingMessages.clear();
|
||||
emit errorOccurred("Ошибка GigaChat: " + errorMsg);
|
||||
} else {
|
||||
pendingMessages.clear();
|
||||
emit errorOccurred("Неизвестный формат ответа от GigaChat");
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
bool NeuralNetworkManager::isGigaChatTokenValid() const
|
||||
{
|
||||
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
// Проверяем есть ли токен и не истек ли он
|
||||
bool isValid = !gigaChatAuth.accessToken.isEmpty() &&
|
||||
gigaChatAuth.tokenExpiry > currentTime;
|
||||
|
||||
qDebug() << "Проверка токена GigaChat:"
|
||||
<< "Токен:" << (!gigaChatAuth.accessToken.isEmpty() ? "есть" : "нет")
|
||||
<< "Текущее время:" << currentTime
|
||||
<< "Истекает:" << gigaChatAuth.tokenExpiry
|
||||
<< "Валиден:" << (isValid ? "да" : "нет");
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
bool NeuralNetworkManager::shouldRefreshGigaChatToken() const
|
||||
{
|
||||
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
// Проверяем, нужно ли обновить токен (осталось меньше 5 минут)
|
||||
bool shouldRefresh = gigaChatAuth.tokenExpiry > 0 &&
|
||||
(gigaChatAuth.tokenExpiry - currentTime) < 300;
|
||||
|
||||
qDebug() << "Нужно ли обновить токен:"
|
||||
<< "Текущее время:" << currentTime
|
||||
<< "Истекает:" << gigaChatAuth.tokenExpiry
|
||||
<< "Осталось секунд:" << (gigaChatAuth.tokenExpiry - currentTime)
|
||||
<< "Обновить:" << (shouldRefresh ? "да" : "нет");
|
||||
|
||||
return shouldRefresh;
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::requestGigaChatToken()
|
||||
{
|
||||
if (isProcessingGigaChatToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gigaChatAuth.clientId.isEmpty() || gigaChatAuth.authorizationCode.isEmpty()) {
|
||||
emit errorOccurred("Не установлены учетные данные GigaChat");
|
||||
|
||||
// Очищаем очередь сообщений
|
||||
pendingMessages.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessingGigaChatToken = true;
|
||||
|
||||
QString tokenUrl = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth";
|
||||
QNetworkRequest request((QUrl(tokenUrl)));
|
||||
|
||||
// Настройка SSL для GigaChat
|
||||
setupSSLForGigaChat();
|
||||
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
||||
request.setSslConfiguration(sslConfig);
|
||||
|
||||
// Заголовки
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
request.setRawHeader("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||
request.setRawHeader("RqUID", gigaChatAuth.clientId.toUtf8());
|
||||
request.setRawHeader("Authorization",
|
||||
QString("Basic %1").arg(gigaChatAuth.authorizationCode).toUtf8());
|
||||
|
||||
// Тело запроса
|
||||
QByteArray postData = "scope=" + gigaChatScope.toUtf8();
|
||||
|
||||
QNetworkReply *reply = networkManager->post(request, postData);
|
||||
reply->setProperty("isTokenRequest", true);
|
||||
|
||||
// Обработка SSL ошибок
|
||||
connect(reply, &QNetworkReply::sslErrors,
|
||||
this, [reply](const QList<QSslError> &errors) {
|
||||
reply->ignoreSslErrors();
|
||||
});
|
||||
}
|
||||
|
||||
void NeuralNetworkManager::processPendingMessages()
|
||||
{
|
||||
|
||||
while (!pendingMessages.isEmpty()) {
|
||||
PendingMessage pendingMsg = pendingMessages.takeFirst();
|
||||
|
||||
// Проверяем, что токен все еще валиден
|
||||
if (isGigaChatTokenValid()) {
|
||||
sendMessageInternal(pendingMsg.message, pendingMsg.networkType);
|
||||
} else {
|
||||
pendingMessages.prepend(pendingMsg); // Возвращаем сообщение в начало очереди
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString NeuralNetworkManager::parseResponse(NetworkType type, const QByteArray &data)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isNull()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonObject json = doc.object();
|
||||
|
||||
if (type == Ollama) {
|
||||
// Парсинг ответа Ollama
|
||||
if (json.contains("message")) {
|
||||
QJsonObject message = json["message"].toObject();
|
||||
if (message.contains("content")) {
|
||||
return message["content"].toString().trimmed();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Парсинг ответа OpenAI-совместимых API
|
||||
if (json.contains("choices") && json["choices"].isArray()) {
|
||||
QJsonArray choices = json["choices"].toArray();
|
||||
if (!choices.isEmpty()) {
|
||||
QJsonObject choice = choices[0].toObject();
|
||||
if (choice.contains("message")) {
|
||||
QJsonObject message = choice["message"].toObject();
|
||||
if (message.contains("content")) {
|
||||
return message["content"].toString().trimmed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка на ошибки API
|
||||
if (json.contains("error")) {
|
||||
QJsonObject error = json["error"].toObject();
|
||||
QString errorMsg = error.contains("message")
|
||||
? error["message"].toString()
|
||||
: "Неизвестная ошибка API";
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString NeuralNetworkManager::truncateResponse(const QString &response)
|
||||
{
|
||||
// Ограничиваем длину ответа 400 символами
|
||||
if (response.length() > 400) {
|
||||
QString truncated = response.left(400);
|
||||
|
||||
// Пытаемся обрезать по последнему предложению
|
||||
int lastDot = truncated.lastIndexOf('.');
|
||||
int lastExcl = truncated.lastIndexOf('!');
|
||||
int lastQuest = truncated.lastIndexOf('?');
|
||||
int lastBreak = qMax(lastDot, qMax(lastExcl, lastQuest));
|
||||
|
||||
if (lastBreak > 300) {
|
||||
truncated = truncated.left(lastBreak + 1);
|
||||
} else {
|
||||
truncated += "...";
|
||||
}
|
||||
|
||||
return truncated;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// neuralnetworkmanager.h
|
||||
#ifndef NEURALNETWORKMANAGER_H
|
||||
#define NEURALNETWORKMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QMutex>
|
||||
#include <QTimer>
|
||||
#include <QSslConfiguration>
|
||||
|
||||
class NeuralNetworkManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum NetworkType {
|
||||
DeepSeek,
|
||||
GigaChat,
|
||||
ChatGPT,
|
||||
Ollama
|
||||
};
|
||||
|
||||
explicit NeuralNetworkManager(QObject *parent = nullptr);
|
||||
~NeuralNetworkManager();
|
||||
|
||||
// Основные методы
|
||||
void sendMessage(const QString &message, NetworkType networkType);
|
||||
void setPrefix(const QString &prefix);
|
||||
void setApiKey(NetworkType networkType, const QString &apiKey);
|
||||
void setModel(NetworkType networkType, const QString &model);
|
||||
void setBaseUrl(NetworkType networkType, const QString &url);
|
||||
|
||||
// Настройки Ollama
|
||||
void setOllamaUrl(const QString &url);
|
||||
|
||||
// Настройки GigaChat OAuth
|
||||
void setGigaChatCredentials(const QString &clientId,
|
||||
const QString &authorizationCode);
|
||||
void refreshGigaChatToken();
|
||||
|
||||
private slots:
|
||||
void handleNetworkReply(QNetworkReply *reply);
|
||||
void handleGigaChatTokenReply(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
struct NetworkConfig {
|
||||
QString apiKey;
|
||||
QString model;
|
||||
QString baseUrl;
|
||||
QString endpoint;
|
||||
};
|
||||
|
||||
struct GigaChatAuth {
|
||||
QString clientId;
|
||||
QString authorizationCode;
|
||||
QString accessToken;
|
||||
qint64 tokenExpiry = 0; // Unix timestamp
|
||||
};
|
||||
|
||||
// SSL настройки
|
||||
void setupSSL();
|
||||
void setupSSLForGigaChat();
|
||||
|
||||
// Основные переменные
|
||||
QNetworkAccessManager *networkManager;
|
||||
QMap<NetworkType, NetworkConfig> networkConfigs;
|
||||
QString currentPrefix;
|
||||
QMutex mutex;
|
||||
GigaChatAuth gigaChatAuth;
|
||||
QString gigaChatScope = "GIGACHAT_API_PERS";
|
||||
bool isProcessingGigaChatToken = false;
|
||||
|
||||
// Методы для подготовки запросов
|
||||
QNetworkRequest prepareRequest(NetworkType type);
|
||||
QByteArray prepareRequestBody(NetworkType type, const QString &message);
|
||||
|
||||
// Методы для парсинга ответов
|
||||
QString parseResponse(NetworkType type, const QByteArray &data);
|
||||
QString truncateResponse(const QString &response);
|
||||
|
||||
// Методы для работы с GigaChat OAuth
|
||||
void requestGigaChatToken();
|
||||
bool isGigaChatTokenValid() const;
|
||||
bool shouldRefreshGigaChatToken() const;
|
||||
|
||||
// Очередь сообщений для GigaChat
|
||||
struct PendingMessage {
|
||||
QString message;
|
||||
NetworkType networkType;
|
||||
};
|
||||
QList<PendingMessage> pendingMessages;
|
||||
void processPendingMessages();
|
||||
void sendMessageInternal(const QString &message, NetworkType networkType);
|
||||
|
||||
// Конфигурация по умолчанию
|
||||
void setupDefaultConfigs();
|
||||
|
||||
signals:
|
||||
void responseReceived(const QString &response);
|
||||
void errorOccurred(const QString &errorMessage);
|
||||
void gigaChatTokenRefreshed();
|
||||
};
|
||||
|
||||
#endif // NEURALNETWORKMANAGER_H
|
||||
@@ -0,0 +1,41 @@
|
||||
debug/emoteprovider.o
|
||||
debug/fcolorsetting.o
|
||||
debug/fcreatechat.o
|
||||
debug/fcreatenotify.o
|
||||
debug/ffontsetting.o
|
||||
debug/fsettingsws.o
|
||||
debug/fsinglegrid.o
|
||||
debug/main.o
|
||||
debug/neuralnetworkmanager.o
|
||||
debug/soundmanager.o
|
||||
debug/tauth.o
|
||||
debug/ttw_api.o
|
||||
debug/twitchmessage.o
|
||||
debug/udatabase.o
|
||||
debug/ugeneral.o
|
||||
debug/ulink.o
|
||||
debug/user.o
|
||||
debug/user_manager.o
|
||||
debug/userwidget.o
|
||||
debug/webserverchat.o
|
||||
debug/webservernotify.o
|
||||
debug/websocketclient.o
|
||||
debug/moc_emoteprovider.o
|
||||
debug/moc_fcolorsetting.o
|
||||
debug/moc_fcreatechat.o
|
||||
debug/moc_fcreatenotify.o
|
||||
debug/moc_ffontsetting.o
|
||||
debug/moc_fsettingsws.o
|
||||
debug/moc_fsinglegrid.o
|
||||
debug/moc_neuralnetworkmanager.o
|
||||
debug/moc_soundmanager.o
|
||||
debug/moc_tauth.o
|
||||
debug/moc_ttw_api.o
|
||||
debug/moc_udatabase.o
|
||||
debug/moc_ugeneral.o
|
||||
debug/moc_ulink.o
|
||||
debug/moc_user_manager.o
|
||||
debug/moc_userwidget.o
|
||||
debug/moc_webserverchat.o
|
||||
debug/moc_webservernotify.o
|
||||
debug/moc_websocketclient.o
|
||||
@@ -0,0 +1,41 @@
|
||||
release/emoteprovider.o
|
||||
release/fcolorsetting.o
|
||||
release/fcreatechat.o
|
||||
release/fcreatenotify.o
|
||||
release/ffontsetting.o
|
||||
release/fsettingsws.o
|
||||
release/fsinglegrid.o
|
||||
release/main.o
|
||||
release/neuralnetworkmanager.o
|
||||
release/soundmanager.o
|
||||
release/tauth.o
|
||||
release/ttw_api.o
|
||||
release/twitchmessage.o
|
||||
release/udatabase.o
|
||||
release/ugeneral.o
|
||||
release/ulink.o
|
||||
release/user.o
|
||||
release/user_manager.o
|
||||
release/userwidget.o
|
||||
release/webserverchat.o
|
||||
release/webservernotify.o
|
||||
release/websocketclient.o
|
||||
release/moc_emoteprovider.o
|
||||
release/moc_fcolorsetting.o
|
||||
release/moc_fcreatechat.o
|
||||
release/moc_fcreatenotify.o
|
||||
release/moc_ffontsetting.o
|
||||
release/moc_fsettingsws.o
|
||||
release/moc_fsinglegrid.o
|
||||
release/moc_neuralnetworkmanager.o
|
||||
release/moc_soundmanager.o
|
||||
release/moc_tauth.o
|
||||
release/moc_ttw_api.o
|
||||
release/moc_udatabase.o
|
||||
release/moc_ugeneral.o
|
||||
release/moc_ulink.o
|
||||
release/moc_user_manager.o
|
||||
release/moc_userwidget.o
|
||||
release/moc_webserverchat.o
|
||||
release/moc_webservernotify.o
|
||||
release/moc_websocketclient.o
|
||||
@@ -0,0 +1,274 @@
|
||||
#include "soundmanager.h"
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
|
||||
SoundManager::SoundManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
// Инициализация COM
|
||||
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
||||
|
||||
// Инициализация каналов
|
||||
for (int i = 0; i < 2; i++) {
|
||||
m_channels[i].graphBuilder = NULL;
|
||||
m_channels[i].mediaControl = NULL;
|
||||
m_channels[i].mediaPosition = NULL;
|
||||
m_channels[i].basicAudio = NULL;
|
||||
m_channels[i].audioRenderer = NULL;
|
||||
m_channels[i].sessionManager = NULL;
|
||||
m_channels[i].sessionControl = NULL;
|
||||
m_channels[i].isLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
SoundManager::~SoundManager()
|
||||
{
|
||||
// Очистка ресурсов
|
||||
for (int i = 0; i < 2; i++) {
|
||||
cleanupChannel(static_cast<SoundChannel>(i));
|
||||
}
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
IBaseFilter* SoundManager::createAudioRenderer()
|
||||
{
|
||||
IBaseFilter* renderer = NULL;
|
||||
|
||||
// Создаем отдельный аудио рендерер для каждого канала
|
||||
HRESULT hr = CoCreateInstance(CLSID_DSoundRender, NULL,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_IBaseFilter,
|
||||
(void**)&renderer);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
bool SoundManager::initializeChannel(SoundChannel channel)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
// Создаем GraphBuilder
|
||||
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_IGraphBuilder,
|
||||
(void**)&m_channels[idx].graphBuilder);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаем отдельный аудио рендерер для этого канала
|
||||
m_channels[idx].audioRenderer = createAudioRenderer();
|
||||
if (!m_channels[idx].audioRenderer) {
|
||||
cleanupChannel(channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Добавляем рендерер в граф
|
||||
WCHAR filterName[64];
|
||||
swprintf_s(filterName, 64, L"Audio Renderer Channel %d", idx + 1);
|
||||
|
||||
hr = m_channels[idx].graphBuilder->AddFilter(m_channels[idx].audioRenderer, filterName);
|
||||
if (FAILED(hr)) {
|
||||
cleanupChannel(channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Пытаемся получить интерфейс аудио сессии для отдельного управления
|
||||
IAudioSessionManager2* pSessionManager = NULL;
|
||||
hr = m_channels[idx].audioRenderer->QueryInterface(__uuidof(IAudioSessionManager2),
|
||||
(void**)&m_channels[idx].sessionManager);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
// Получаем контроль над сессией
|
||||
hr = m_channels[idx].sessionManager->GetAudioSessionControl(NULL, 0,
|
||||
&m_channels[idx].sessionControl);
|
||||
if (SUCCEEDED(hr)) {
|
||||
// Устанавливаем уникальное отображаемое имя для сессии
|
||||
WCHAR sessionName[64];
|
||||
swprintf_s(sessionName, 64, L"Channel %d", idx + 1);
|
||||
m_channels[idx].sessionControl->SetDisplayName(sessionName, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем интерфейсы управления
|
||||
hr = m_channels[idx].graphBuilder->QueryInterface(IID_IMediaControl,
|
||||
(void**)&m_channels[idx].mediaControl);
|
||||
if (FAILED(hr)) {
|
||||
cleanupChannel(channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = m_channels[idx].graphBuilder->QueryInterface(IID_IMediaPosition,
|
||||
(void**)&m_channels[idx].mediaPosition);
|
||||
if (FAILED(hr)) {
|
||||
cleanupChannel(channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = m_channels[idx].graphBuilder->QueryInterface(IID_IBasicAudio,
|
||||
(void**)&m_channels[idx].basicAudio);
|
||||
if (FAILED(hr)) {
|
||||
cleanupChannel(channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundManager::cleanupChannel(SoundChannel channel)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
if (m_channels[idx].mediaControl) {
|
||||
m_channels[idx].mediaControl->Stop();
|
||||
m_channels[idx].mediaControl->Release();
|
||||
m_channels[idx].mediaControl = NULL;
|
||||
}
|
||||
|
||||
if (m_channels[idx].mediaPosition) {
|
||||
m_channels[idx].mediaPosition->Release();
|
||||
m_channels[idx].mediaPosition = NULL;
|
||||
}
|
||||
|
||||
if (m_channels[idx].basicAudio) {
|
||||
m_channels[idx].basicAudio->Release();
|
||||
m_channels[idx].basicAudio = NULL;
|
||||
}
|
||||
|
||||
if (m_channels[idx].sessionControl) {
|
||||
m_channels[idx].sessionControl->Release();
|
||||
m_channels[idx].sessionControl = NULL;
|
||||
}
|
||||
|
||||
if (m_channels[idx].sessionManager) {
|
||||
m_channels[idx].sessionManager->Release();
|
||||
m_channels[idx].sessionManager = NULL;
|
||||
}
|
||||
|
||||
if (m_channels[idx].audioRenderer) {
|
||||
m_channels[idx].audioRenderer->Release();
|
||||
m_channels[idx].audioRenderer = NULL;
|
||||
}
|
||||
|
||||
if (m_channels[idx].graphBuilder) {
|
||||
m_channels[idx].graphBuilder->Release();
|
||||
m_channels[idx].graphBuilder = NULL;
|
||||
}
|
||||
|
||||
m_channels[idx].isLoaded = false;
|
||||
}
|
||||
|
||||
bool SoundManager::loadSound(SoundChannel channel, const QString &filePath)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
// Проверяем существование файла
|
||||
if (!QFileInfo(filePath).exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Очищаем предыдущий звук на канале
|
||||
cleanupChannel(channel);
|
||||
|
||||
// Инициализируем канал
|
||||
if (!initializeChannel(channel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Конвертируем QString в WCHAR
|
||||
std::wstring wideFilePath = filePath.toStdWString();
|
||||
|
||||
// Рендерим файл
|
||||
HRESULT hr = m_channels[idx].graphBuilder->RenderFile(wideFilePath.c_str(), NULL);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
cleanupChannel(channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_channels[idx].isLoaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundManager::playSound(SoundChannel channel)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
if (!m_channels[idx].isLoaded || !m_channels[idx].mediaControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT hr = m_channels[idx].mediaControl->Run();
|
||||
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundManager::pauseSound(SoundChannel channel)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
if (!m_channels[idx].isLoaded || !m_channels[idx].mediaControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT hr = m_channels[idx].mediaControl->Pause();
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
bool SoundManager::stopSound(SoundChannel channel)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
if (!m_channels[idx].isLoaded || !m_channels[idx].mediaControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT hr = m_channels[idx].mediaControl->Stop();
|
||||
|
||||
// Сбрасываем позицию воспроизведения
|
||||
if (m_channels[idx].mediaPosition) {
|
||||
m_channels[idx].mediaPosition->put_CurrentPosition(0);
|
||||
}
|
||||
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
void SoundManager::setVolume(SoundChannel channel, int volume)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
if (!m_channels[idx].isLoaded || !m_channels[idx].basicAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Конвертируем 0-100 в диапазон DirectShow (-10000 до 0)
|
||||
long directShowVolume = (volume - 100) * 100; // 0 = -10000, 100 = 0
|
||||
|
||||
m_channels[idx].basicAudio->put_Volume(directShowVolume);
|
||||
}
|
||||
|
||||
bool SoundManager::isPlaying(SoundChannel channel)
|
||||
{
|
||||
int idx = static_cast<int>(channel);
|
||||
|
||||
if (!m_channels[idx].isLoaded || !m_channels[idx].mediaControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OAFilterState state;
|
||||
HRESULT hr = m_channels[idx].mediaControl->GetState(10, &state);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
return (state == State_Running);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef SOUNDMANAGER_H
|
||||
#define SOUNDMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <windows.h>
|
||||
#include <dshow.h>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <audioclient.h>
|
||||
#include <audiopolicy.h>
|
||||
|
||||
class SoundManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SoundManager(QObject *parent = 0);
|
||||
~SoundManager();
|
||||
|
||||
enum SoundChannel {
|
||||
Channel1 = 0,
|
||||
Channel2 = 1
|
||||
};
|
||||
|
||||
bool loadSound(SoundChannel channel, const QString &filePath);
|
||||
bool playSound(SoundChannel channel);
|
||||
bool pauseSound(SoundChannel channel);
|
||||
bool stopSound(SoundChannel channel);
|
||||
void setVolume(SoundChannel channel, int volume); // 0-100
|
||||
bool isPlaying(SoundChannel channel);
|
||||
|
||||
private:
|
||||
struct SoundDevice {
|
||||
IGraphBuilder *graphBuilder;
|
||||
IMediaControl *mediaControl;
|
||||
IMediaPosition *mediaPosition;
|
||||
IBasicAudio *basicAudio;
|
||||
IBaseFilter *audioRenderer;
|
||||
IAudioSessionManager2 *sessionManager;
|
||||
IAudioSessionControl *sessionControl;
|
||||
bool isLoaded;
|
||||
};
|
||||
|
||||
SoundDevice m_channels[2];
|
||||
|
||||
bool initializeChannel(SoundChannel channel);
|
||||
void cleanupChannel(SoundChannel channel);
|
||||
IBaseFilter* createAudioRenderer();
|
||||
};
|
||||
|
||||
#endif // SOUNDMANAGER_H
|
||||
@@ -0,0 +1,371 @@
|
||||
#include "tauth.h"
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QDesktopServices>
|
||||
#include <QHostAddress>
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
TAuth::TAuth(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_server(nullptr)
|
||||
, m_clientSocket(nullptr)
|
||||
, m_serverPort(0)
|
||||
, m_autoOpenBrowser(true)
|
||||
{
|
||||
}
|
||||
|
||||
TAuth::~TAuth()
|
||||
{
|
||||
stopServer();
|
||||
}
|
||||
|
||||
void TAuth::startServer(const QString &authUrl, bool openBrowser)
|
||||
{
|
||||
stopServer();
|
||||
|
||||
m_authUrl = authUrl;
|
||||
m_autoOpenBrowser = openBrowser;
|
||||
m_server = new QTcpServer(this);
|
||||
|
||||
// Пытаемся слушать на портах, начиная с 8080
|
||||
QVector<int> portsToTry = {8089};
|
||||
|
||||
for (int port : portsToTry) {
|
||||
if (m_server->listen(QHostAddress::LocalHost, port)) {
|
||||
m_serverPort = port;
|
||||
emit serverStarted(port);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_server->isListening()) {
|
||||
emit errorOccurred("Не удалось запустить сервер на доступных портах");
|
||||
delete m_server;
|
||||
m_server = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
connect(m_server, &QTcpServer::newConnection,
|
||||
this, &TAuth::handleNewConnection);
|
||||
|
||||
// Открываем браузер только если указано
|
||||
if (openBrowser && !authUrl.isEmpty()) {
|
||||
QDesktopServices::openUrl(QUrl(authUrl));
|
||||
}
|
||||
}
|
||||
|
||||
void TAuth::stopServer()
|
||||
{
|
||||
if (m_server) {
|
||||
m_server->close();
|
||||
m_server->deleteLater();
|
||||
m_server = nullptr;
|
||||
}
|
||||
|
||||
if (m_clientSocket) {
|
||||
m_clientSocket->disconnect();
|
||||
m_clientSocket->close();
|
||||
m_clientSocket->deleteLater();
|
||||
m_clientSocket = nullptr;
|
||||
}
|
||||
|
||||
m_serverPort = 0;
|
||||
}
|
||||
|
||||
void TAuth::handleNewConnection()
|
||||
{
|
||||
if (!m_server) return;
|
||||
|
||||
m_clientSocket = m_server->nextPendingConnection();
|
||||
if (m_clientSocket) {
|
||||
connect(m_clientSocket, &QTcpSocket::readyRead,
|
||||
this, &TAuth::readClientData);
|
||||
connect(m_clientSocket, &QTcpSocket::disconnected,
|
||||
m_clientSocket, &QTcpSocket::deleteLater);
|
||||
}
|
||||
}
|
||||
|
||||
void TAuth::readClientData()
|
||||
{
|
||||
if (!m_clientSocket || !m_clientSocket->bytesAvailable()) return;
|
||||
|
||||
QByteArray requestData = m_clientSocket->readAll();
|
||||
QString request = QString::fromUtf8(requestData);
|
||||
|
||||
|
||||
QStringList lines = request.split("\r\n");
|
||||
if (lines.isEmpty()) return;
|
||||
|
||||
QString requestLine = lines[0];
|
||||
QStringList parts = requestLine.split(" ");
|
||||
|
||||
if (parts.size() >= 2 && parts[0].toUpper() == "GET") {
|
||||
QString document = parts[1];
|
||||
|
||||
// Проверяем, есть ли в запросе параметры (они могут быть в теле запроса)
|
||||
// или в реферере
|
||||
QString referer;
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith("Referer:", Qt::CaseInsensitive)) {
|
||||
referer = line.mid(9).trimmed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Обрабатываем обычный redirect
|
||||
if (document.startsWith("/redirect")) {
|
||||
handleRedirectRequest(document, m_clientSocket);
|
||||
}
|
||||
// Обрабатываем корневой запрос
|
||||
else if (document == "/" || document == "/?") {
|
||||
// Проверяем, есть ли хэш в URL
|
||||
QString fullUrl = document;
|
||||
if (!referer.isEmpty() && referer.contains("#")) {
|
||||
// Извлекаем хэш из реферера
|
||||
QString hash = referer.mid(referer.indexOf("#") + 1);
|
||||
fullUrl = "/redirect?" + hash;
|
||||
handleRedirectRequest(fullUrl, m_clientSocket);
|
||||
} else {
|
||||
// Или проверяем, есть ли параметры в самом запросе
|
||||
// (хэш не передается, но могут быть другие параметры)
|
||||
QString params;
|
||||
if (document.contains("#")) {
|
||||
params = document.mid(document.indexOf("#") + 1);
|
||||
fullUrl = "/redirect?" + params;
|
||||
handleRedirectRequest(fullUrl, m_clientSocket);
|
||||
} else {
|
||||
handleRootRequest(m_clientSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (document.startsWith("/da")) {
|
||||
QString html =
|
||||
"<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>Redirecting...</title>\n</head>\n<body>\n"
|
||||
" <p>получаю код</p>\n<script>\n"
|
||||
"var paragraph = window.location.href;\n"
|
||||
"var urrl = paragraph.replace('localhost:8089/da','localhost:8089/redirect');\n"
|
||||
"urrl = urrl.replace('#','?');\n"
|
||||
"console.log(urrl);\n"
|
||||
"window.location.href = urrl;\n"
|
||||
"</script>\n</body>\n</html>";
|
||||
sendResponse(m_clientSocket, html);
|
||||
}
|
||||
else {
|
||||
// Показываем страницу с JavaScript для извлечения хэша
|
||||
QString html =
|
||||
"<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>Processing...</title>\n</head>\n<body>\n"
|
||||
" <p>Обработка авторизации...</p>\n<script>\n"
|
||||
"try {\n"
|
||||
" // Проверяем, есть ли хэш в URL\n"
|
||||
" var hash = window.location.hash;\n"
|
||||
" if (hash) {\n"
|
||||
" // Преобразуем # в ? для передачи на сервер\n"
|
||||
" var params = hash.substring(1); // убираем #\n"
|
||||
" // Перенаправляем на /redirect с параметрами\n"
|
||||
" window.location.href = '/redirect?' + params;\n"
|
||||
" } else {\n"
|
||||
" document.body.innerHTML = '<h2>Нет данных авторизации</h2>';\n"
|
||||
" }\n"
|
||||
"} catch(e) {\n"
|
||||
" document.body.innerHTML = '<h2>Ошибка: ' + e.message + '</h2>';\n"
|
||||
"}\n"
|
||||
"</script>\n</body>\n</html>";
|
||||
sendResponse(m_clientSocket, html);
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(m_clientSocket, "Invalid request method");
|
||||
}
|
||||
}
|
||||
|
||||
void TAuth::handleRootRequest(QTcpSocket *socket)
|
||||
{
|
||||
QString html =
|
||||
"<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>Processing...</title>\n</head>\n<body>\n"
|
||||
" <p>Обработка авторизации...</p>\n<script>\n"
|
||||
"try {\n"
|
||||
" var hash = window.location.hash;\n"
|
||||
" if (hash) {\n"
|
||||
" var params = hash.substring(1);\n"
|
||||
" window.location.href = '/redirect?' + params;\n"
|
||||
" } else {\n"
|
||||
" document.body.innerHTML = '<h2>Нет данных авторизации в URL</h2>';\n"
|
||||
" }\n"
|
||||
"} catch(e) {\n"
|
||||
" document.body.innerHTML = '<h2>Ошибка: ' + e.message + '</h2>';\n"
|
||||
"}\n"
|
||||
"</script>\n</body>\n</html>";
|
||||
|
||||
sendResponse(socket, html);
|
||||
}
|
||||
|
||||
void TAuth::handleRedirectRequest(const QString &request, QTcpSocket *socket)
|
||||
{
|
||||
QString params;
|
||||
if (request.contains('?')) {
|
||||
params = request.mid(request.indexOf('?') + 1);
|
||||
}
|
||||
|
||||
QString html;
|
||||
QString token, code, expiresIn;
|
||||
|
||||
// Проверяем наличие access_token
|
||||
if (params.contains("access_token=")) {
|
||||
token = extractParam(params, "access_token");
|
||||
expiresIn = extractExpiresIn(params);
|
||||
|
||||
// Формируем полный токен с временем жизни (если есть)
|
||||
QString fullToken = token;
|
||||
if (!expiresIn.isEmpty()) {
|
||||
fullToken += "|" + expiresIn;
|
||||
}
|
||||
|
||||
emit tokenReceived(fullToken);
|
||||
|
||||
html =
|
||||
"<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>Success</title>\n</head>\n<body>\n"
|
||||
"<h2>Authorization Successful!</h2>\n"
|
||||
"<p>Token received. You can close this window.</p>\n"
|
||||
"</body>\n</html>";
|
||||
}
|
||||
// Проверяем наличие error_description
|
||||
else if (params.contains("error_description=")) {
|
||||
QString error = extractErrorDescription(params);
|
||||
emit errorOccurred(error);
|
||||
|
||||
html =
|
||||
QString("<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>Error</title>\n</head>\n<body>\n"
|
||||
"<h2>Authorization Failed</h2>\n"
|
||||
"<p>Error: %1</p>\n</body>\n</html>").arg(error);
|
||||
}
|
||||
// Проверяем наличие code (для OAuth code flow)
|
||||
else if (params.contains("code=")) {
|
||||
code = extractParam(params, "code");
|
||||
emit codeReceived(code);
|
||||
|
||||
html =
|
||||
"<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>Success</title>\n</head>\n<body>\n"
|
||||
"<h2>Authorization Successful!</h2>\n"
|
||||
"<p>Code received. You can close this window.</p>\n"
|
||||
"</body>\n</html>";
|
||||
}
|
||||
else {
|
||||
html =
|
||||
"<!DOCTYPE html>\n<html>\n<head>\n"
|
||||
" <title>No Data</title>\n</head>\n<body>\n"
|
||||
"<h2>No authorization data received</h2>\n"
|
||||
"<p>Try again or check your authorization URL.</p>\n"
|
||||
"</body>\n</html>";
|
||||
sendResponse(socket, html);
|
||||
return;
|
||||
}
|
||||
|
||||
sendResponse(socket, html);
|
||||
|
||||
// Останавливаем сервер после обработки
|
||||
QTimer::singleShot(1000, this, &TAuth::stopServer);
|
||||
}
|
||||
|
||||
QString TAuth::extractParam(const QString ¶ms, const QString ¶mName)
|
||||
{
|
||||
QStringList paramList = params.split('&');
|
||||
for (const QString ¶m : paramList) {
|
||||
if (param.startsWith(paramName + "=")) {
|
||||
QString value = param.mid(paramName.length() + 1);
|
||||
// Декодируем URL-encoded значения
|
||||
value = QUrl::fromPercentEncoding(value.toUtf8());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString TAuth::extractExpiresIn(const QString ¶ms)
|
||||
{
|
||||
return extractParam(params, "expires_in");
|
||||
}
|
||||
|
||||
void TAuth::extractAndEmitToken(const QString ¶ms)
|
||||
{
|
||||
QString token = extractParam(params, "access_token");
|
||||
QString expiresIn = extractExpiresIn(params);
|
||||
|
||||
if (!token.isEmpty()) {
|
||||
QString fullToken = token;
|
||||
if (!expiresIn.isEmpty()) {
|
||||
fullToken += "|" + expiresIn;
|
||||
}
|
||||
emit tokenReceived(fullToken);
|
||||
}
|
||||
}
|
||||
|
||||
void TAuth::extractAndEmitError(const QString ¶ms)
|
||||
{
|
||||
QString error = extractErrorDescription(params);
|
||||
if (!error.isEmpty()) {
|
||||
emit errorOccurred(error);
|
||||
}
|
||||
}
|
||||
|
||||
void TAuth::extractAndEmitCode(const QString ¶ms)
|
||||
{
|
||||
QString code = extractParam(params, "code");
|
||||
if (!code.isEmpty()) {
|
||||
emit codeReceived(code);
|
||||
}
|
||||
}
|
||||
|
||||
QString TAuth::extractErrorDescription(const QString ¶ms)
|
||||
{
|
||||
QString error = extractParam(params, "error_description");
|
||||
if (error.isEmpty()) {
|
||||
error = extractParam(params, "error");
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
error = "Unknown error";
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
void TAuth::sendResponse(QTcpSocket *socket, const QString &content)
|
||||
{
|
||||
if (!socket) return;
|
||||
|
||||
QString response =
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
"Content-Length: " + QString::number(content.toUtf8().size()) + "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n" + content;
|
||||
|
||||
socket->write(response.toUtf8());
|
||||
socket->flush();
|
||||
}
|
||||
|
||||
void TAuth::sendErrorResponse(QTcpSocket *socket, const QString &error)
|
||||
{
|
||||
if (!socket) return;
|
||||
|
||||
QString content = "<html><body><h1>" + error + "</h1></body></html>";
|
||||
QString response =
|
||||
"HTTP/1.1 404 Not Found\r\n"
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
"Content-Length: " + QString::number(content.toUtf8().size()) + "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n" + content;
|
||||
|
||||
socket->write(response.toUtf8());
|
||||
socket->flush();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef TAUTH_H
|
||||
#define TAUTH_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QTcpServer;
|
||||
class QTcpSocket;
|
||||
|
||||
class TAuth : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TAuth(QObject *parent = nullptr);
|
||||
~TAuth();
|
||||
|
||||
// Запуск сервера с указанием режима работы
|
||||
void startServer(const QString &authUrl, bool openBrowser = true);
|
||||
void stopServer();
|
||||
|
||||
// Получить URL для отображения (если не открываем браузер)
|
||||
QString getAuthUrl() const { return m_authUrl; }
|
||||
|
||||
// Получить порт, на котором запущен сервер
|
||||
int getServerPort() const { return m_serverPort; }
|
||||
|
||||
signals:
|
||||
void tokenReceived(const QString &token);
|
||||
void errorOccurred(const QString &error);
|
||||
// Новый сигнал для передачи кода авторизации
|
||||
void codeReceived(const QString &code);
|
||||
// Сигнал о запуске сервера
|
||||
void serverStarted(int port);
|
||||
|
||||
private slots:
|
||||
void handleNewConnection();
|
||||
void readClientData();
|
||||
|
||||
private:
|
||||
QTcpServer *m_server;
|
||||
QTcpSocket *m_clientSocket;
|
||||
QString m_authUrl;
|
||||
int m_serverPort;
|
||||
bool m_autoOpenBrowser;
|
||||
|
||||
void handleRootRequest(QTcpSocket *socket);
|
||||
void handleRedirectRequest(const QString &request, QTcpSocket *socket);
|
||||
void sendResponse(QTcpSocket *socket, const QString &content);
|
||||
void sendErrorResponse(QTcpSocket *socket, const QString &error);
|
||||
|
||||
void extractAndEmitToken(const QString ¶ms);
|
||||
void extractAndEmitError(const QString ¶ms);
|
||||
void extractAndEmitCode(const QString ¶ms);
|
||||
QString extractErrorDescription(const QString ¶ms);
|
||||
|
||||
// Вспомогательные методы для извлечения параметров
|
||||
QString extractParam(const QString ¶ms, const QString ¶mName);
|
||||
QString extractExpiresIn(const QString ¶ms);
|
||||
};
|
||||
|
||||
#endif // TAUTH_H
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef TIMERINFO_H
|
||||
#define TIMERINFO_H
|
||||
|
||||
#include <QTimer>
|
||||
#include <QString>
|
||||
|
||||
struct TimerInfo {
|
||||
int id;
|
||||
QString message;
|
||||
int interval; // в минутах
|
||||
bool isActive;
|
||||
bool isAnnouncement;
|
||||
QTimer* timer;
|
||||
|
||||
TimerInfo() : id(-1), interval(10), isActive(true), isAnnouncement(false), timer(nullptr) {}
|
||||
};
|
||||
|
||||
#endif // TIMERINFO_H
|
||||
@@ -0,0 +1,593 @@
|
||||
#include "ttw_api.h"
|
||||
#include "qeventloop.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrlQuery>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
|
||||
TTwAPI::TTwAPI(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
{
|
||||
// Настройка таймаутов
|
||||
//m_networkManager->Timeout(30000); // 30 секунд
|
||||
}
|
||||
|
||||
TTwAPI::~TTwAPI()
|
||||
{
|
||||
m_networkManager->deleteLater();
|
||||
}
|
||||
|
||||
void TTwAPI::init(const QString &clientId,
|
||||
const QString &token,
|
||||
const QString &streamerToken,
|
||||
const QString &channel,
|
||||
const QString &botName)
|
||||
{
|
||||
m_clientId = clientId;
|
||||
m_tokenApi = token;
|
||||
m_tokenApiStreamer = streamerToken;
|
||||
m_channelName = channel;
|
||||
m_botName = botName;
|
||||
|
||||
toLog(1, "TTwAPI::init", "API инициализирован для канала: " + channel);
|
||||
}
|
||||
|
||||
QString TTwAPI::sendRequest(const QString &url,
|
||||
const QString &method,
|
||||
const QByteArray &data,
|
||||
const QString &token)
|
||||
{
|
||||
QNetworkRequest request(url);
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Client-ID", m_clientId.toUtf8());
|
||||
QString authHeader = token.startsWith("Bearer") ? token : "Bearer " + token;
|
||||
request.setRawHeader("Authorization", authHeader.toUtf8());
|
||||
|
||||
QNetworkReply *reply = nullptr;
|
||||
|
||||
if (method.toUpper() == "GET") {
|
||||
reply = m_networkManager->get(request);
|
||||
} else if (method.toUpper() == "POST") {
|
||||
reply = m_networkManager->post(request, data);
|
||||
} else if (method.toUpper() == "DELETE") {
|
||||
reply = m_networkManager->sendCustomRequest(request, "DELETE", data);
|
||||
} else if (method.toUpper() == "PATCH") {
|
||||
reply = m_networkManager->sendCustomRequest(request, "PATCH", data);
|
||||
} else if (method.toUpper() == "PUT") {
|
||||
reply = m_networkManager->sendCustomRequest(request, "PUT", data);
|
||||
}
|
||||
|
||||
if (!reply) {
|
||||
toLog(2, "TTwAPI::sendRequest", "Не удалось создать запрос: " + url);
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Ожидание завершения запроса (синхронно)
|
||||
QEventLoop loop;
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
// Проверка ошибок
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMsg = QString("Ошибка %1: %2").arg(reply->error()).arg(reply->errorString());
|
||||
toLog(2, "TTwAPI::sendRequest", errorMsg);
|
||||
|
||||
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
emit tokenExpired(errorMsg);
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString response = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
QString TTwAPI::getTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
bool isStreamer)
|
||||
{
|
||||
Q_UNUSED(clientId);
|
||||
QString baseUrl = "https://api.twitch.tv/helix/";
|
||||
QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi;
|
||||
|
||||
return sendRequest(baseUrl + method, "GET", QByteArray(), token);
|
||||
}
|
||||
|
||||
QString TTwAPI::deleteTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
bool isStreamer)
|
||||
{
|
||||
Q_UNUSED(clientId);
|
||||
QString baseUrl = "https://api.twitch.tv/helix/";
|
||||
QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi;
|
||||
|
||||
return sendRequest(baseUrl + method, "DELETE", QByteArray(), token);
|
||||
}
|
||||
|
||||
QString TTwAPI::postTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
const QByteArray &data,
|
||||
bool isStreamer)
|
||||
{
|
||||
Q_UNUSED(clientId);
|
||||
QString baseUrl = "https://api.twitch.tv/helix/";
|
||||
QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi;
|
||||
|
||||
return sendRequest(baseUrl + method, "POST", data, token);
|
||||
}
|
||||
|
||||
QString TTwAPI::patchTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
const QByteArray &data,
|
||||
bool isStreamer)
|
||||
{
|
||||
Q_UNUSED(clientId);
|
||||
QString baseUrl = "https://api.twitch.tv/helix/";
|
||||
QString token = isStreamer ? m_tokenApiStreamer : m_tokenApi;
|
||||
|
||||
return sendRequest(baseUrl + method, "PATCH", data, token);
|
||||
}
|
||||
|
||||
void TTwAPI::getTTWStat(const QString &channel,
|
||||
int &avgViewers,
|
||||
int &maxViewers,
|
||||
int &hoursWatched,
|
||||
int &followers,
|
||||
int &followersTotal)
|
||||
{
|
||||
Q_UNUSED(channel);
|
||||
QString response = getTTW("channels?broadcaster_id=" + getRoomId(), m_clientId);
|
||||
|
||||
if (response.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getTTWStat", "Пустой ответ от API");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
toLog(2, "TTwAPI::getTTWStat", "Неверный JSON формат");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getTTWStat", "Нет данных в ответе");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject channelData = data[0].toObject();
|
||||
|
||||
// Парсинг данных (заглушка - реальные поля могут отличаться)
|
||||
avgViewers = channelData["average_viewers"].toInt();
|
||||
maxViewers = channelData["peak_viewers"].toInt();
|
||||
hoursWatched = channelData["hours_watched"].toInt();
|
||||
followers = channelData["followers_gained"].toInt();
|
||||
followersTotal = channelData["followers_total"].toInt();
|
||||
}
|
||||
|
||||
QDate TTwAPI::getFollow(const QString &id)
|
||||
{
|
||||
// Правильный endpoint для проверки, является ли пользователь фоловером
|
||||
QString response = getTTW(
|
||||
QString("channels/followers?user_id=%1&broadcaster_id=%2")
|
||||
.arg(id)
|
||||
.arg(getRoomId()),
|
||||
m_clientId,
|
||||
true
|
||||
);
|
||||
|
||||
if (response.isEmpty()) {
|
||||
return QDate();
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return QDate();
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
// Пользователь не фоловер
|
||||
return QDate();
|
||||
}
|
||||
|
||||
QJsonObject followData = data[0].toObject();
|
||||
QString followedAt = followData["followed_at"].toString();
|
||||
|
||||
return QDate::fromString(followedAt, Qt::ISODate);
|
||||
}
|
||||
|
||||
User TTwAPI::getUserByLogin(const QString &login)
|
||||
{
|
||||
User user;
|
||||
user.login = login.toLower();
|
||||
|
||||
QString response = getTTW("users?login=" + login, m_clientId);
|
||||
|
||||
if (response.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getUserByLogin", "Не удалось получить данные пользователя: " + login);
|
||||
return user;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return user;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return user;
|
||||
}
|
||||
|
||||
QJsonObject userData = data[0].toObject();
|
||||
|
||||
user.id = userData["id"].toString();
|
||||
user.displayName = userData["display_name"].toString();
|
||||
user.createdAt = QDate::fromString(userData["created_at"].toString(), Qt::ISODate);
|
||||
|
||||
// Получаем информацию о подписке
|
||||
user.followAt = getFollow(user.id);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
void TTwAPI::setModerator(const QString &id)
|
||||
{
|
||||
QJsonDocument doc((QJsonObject()));
|
||||
postTTW("moderation/moderators?broadcaster_id=" + getRoomId() +
|
||||
"&user_id=" + id,
|
||||
m_clientId,
|
||||
doc.toJson(),
|
||||
true);
|
||||
}
|
||||
|
||||
void TTwAPI::delModerator(const QString &id)
|
||||
{
|
||||
deleteTTW("moderation/moderators?broadcaster_id=" + getRoomId() +
|
||||
"&user_id=" + id, m_clientId, true);
|
||||
}
|
||||
|
||||
void TTwAPI::setVIP(const QString &id)
|
||||
{
|
||||
QJsonDocument doc((QJsonObject()));
|
||||
postTTW("channels/vips?broadcaster_id=" + getRoomId() +
|
||||
"&user_id=" + id,
|
||||
m_clientId,
|
||||
doc.toJson(),
|
||||
true);
|
||||
}
|
||||
|
||||
void TTwAPI::delVIP(const QString &id)
|
||||
{
|
||||
deleteTTW("channels/vips?broadcaster_id=" + getRoomId() +
|
||||
"&user_id=" + id, m_clientId, true);
|
||||
}
|
||||
|
||||
void TTwAPI::banUser(const QString &id)
|
||||
{
|
||||
QString roomId = getRoomId();
|
||||
if (roomId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::banUser", "Не удалось получить roomId");
|
||||
return;
|
||||
}
|
||||
|
||||
QString botId = getBotId(); // Нужно получить ID бота
|
||||
if (botId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::banUser", "Не удалось получить botId");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject innerData;
|
||||
innerData["user_id"] = id;
|
||||
innerData["reason"] = "Нарушение правил чата";
|
||||
|
||||
QJsonObject dataObj;
|
||||
dataObj["data"] = innerData;
|
||||
|
||||
QJsonDocument doc(dataObj);
|
||||
|
||||
postTTW("moderation/bans?broadcaster_id=" + roomId +
|
||||
"&moderator_id=" + botId,
|
||||
m_clientId,
|
||||
doc.toJson(),
|
||||
false);
|
||||
}
|
||||
|
||||
void TTwAPI::banUserTime(const QString &id, int timeMinutes)
|
||||
{
|
||||
QString roomId = getRoomId();
|
||||
if (roomId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::banUserTime", "Не удалось получить roomId");
|
||||
return;
|
||||
}
|
||||
|
||||
QString botId = getBotId();
|
||||
if (botId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::banUserTime", "Не удалось получить botId");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject innerData;
|
||||
innerData["user_id"] = id;
|
||||
innerData["duration"] = timeMinutes * 60;
|
||||
innerData["reason"] = "Таймаут";
|
||||
|
||||
QJsonObject dataObj;
|
||||
dataObj["data"] = innerData;
|
||||
|
||||
QJsonDocument doc(dataObj);
|
||||
|
||||
postTTW("moderation/bans?broadcaster_id=" + roomId +
|
||||
"&moderator_id=" + botId,
|
||||
m_clientId,
|
||||
doc.toJson(),
|
||||
false);
|
||||
}
|
||||
|
||||
void TTwAPI::warnUser(const QString &id)
|
||||
{
|
||||
QString roomId = getRoomId();
|
||||
if (roomId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::warnUser", "Не удалось получить roomId");
|
||||
return;
|
||||
}
|
||||
|
||||
QString botId = getBotId();
|
||||
if (botId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::warnUser", "Не удалось получить botId");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject innerData;
|
||||
innerData["user_id"] = id;
|
||||
|
||||
QJsonObject dataObj;
|
||||
dataObj["data"] = innerData;
|
||||
|
||||
QJsonDocument doc(dataObj);
|
||||
|
||||
postTTW("moderation/warnings?broadcaster_id=" + roomId +
|
||||
"&moderator_id=" + botId,
|
||||
m_clientId,
|
||||
doc.toJson(),
|
||||
false);
|
||||
}
|
||||
|
||||
void TTwAPI::unbanUser(const QString &id)
|
||||
{
|
||||
QString roomId = getRoomId();
|
||||
if (roomId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::unbanUser", "Не удалось получить roomId");
|
||||
return;
|
||||
}
|
||||
|
||||
QString botId = getBotId();
|
||||
if (botId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::unbanUser", "Не удалось получить botId");
|
||||
return;
|
||||
}
|
||||
|
||||
deleteTTW("moderation/bans?broadcaster_id=" + roomId +
|
||||
"&moderator_id=" + botId +
|
||||
"&user_id=" + id,
|
||||
m_clientId,
|
||||
false);
|
||||
}
|
||||
|
||||
QString TTwAPI::getBotId()
|
||||
{
|
||||
static QString cachedBotId;
|
||||
|
||||
if (!cachedBotId.isEmpty()) {
|
||||
return cachedBotId;
|
||||
}
|
||||
|
||||
if (m_botName.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getBotId", "Имя бота не установлено");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString response = getTTW("users?login=" + m_botName, m_clientId);
|
||||
|
||||
if (response.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getBotId", "Не удалось получить данные бота");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
toLog(2, "TTwAPI::getBotId", "Неверный JSON формат");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getBotId", "Нет данных о боте");
|
||||
return QString();
|
||||
}
|
||||
|
||||
cachedBotId = data[0].toObject()["id"].toString();
|
||||
return cachedBotId;
|
||||
}
|
||||
|
||||
QString TTwAPI::getFollowedAtFromJson(const QString &jsonString)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonObject followData = data[0].toObject();
|
||||
return followData["followed_at"].toString();
|
||||
}
|
||||
|
||||
QString TTwAPI::getRoomId()
|
||||
{
|
||||
// Кэширование ID комнаты
|
||||
static QString cachedRoomId;
|
||||
|
||||
if (!cachedRoomId.isEmpty()) {
|
||||
return cachedRoomId;
|
||||
}
|
||||
|
||||
QString response = getTTW("users?login=" + m_channelName, m_clientId);
|
||||
|
||||
if (response.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
cachedRoomId = data[0].toObject()["id"].toString();
|
||||
return cachedRoomId;
|
||||
}
|
||||
|
||||
QString TTwAPI::getRoomAndBot()
|
||||
{
|
||||
QString roomId = getRoomId();
|
||||
QString botId;
|
||||
|
||||
QString response = getTTW("users?login=" + m_botName, m_clientId);
|
||||
|
||||
if (!response.isEmpty()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
botId = data[0].toObject()["id"].toString();
|
||||
}
|
||||
}
|
||||
|
||||
return QString("RoomID: %1, BotID: %2").arg(roomId).arg(botId);
|
||||
}
|
||||
|
||||
void TTwAPI::toLog(int level, const QString &method, const QString &message)
|
||||
{
|
||||
Q_UNUSED(level);
|
||||
Q_UNUSED(method);
|
||||
Q_UNUSED(message);
|
||||
|
||||
// uGeneral.toLog("ttw_api", method, message, level);
|
||||
}
|
||||
|
||||
void TTwAPI::sendAnnouncement(const QString &message) {
|
||||
QString roomId = getRoomId();
|
||||
if (roomId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::sendAnnouncement", "Не удалось получить roomId");
|
||||
return;
|
||||
}
|
||||
|
||||
QString botId = getBotId();
|
||||
if (botId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::sendAnnouncement", "Не удалось получить botId");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json;
|
||||
json["message"] = message;
|
||||
json["color"] = "primary"; // Можно сделать настраиваемым: "primary", "blue", "green", "orange", "purple"
|
||||
QJsonDocument doc(json);
|
||||
QString otv = postTTW("chat/announcements?broadcaster_id=" + roomId +
|
||||
"&moderator_id=" + botId,
|
||||
m_clientId,
|
||||
doc.toJson(),
|
||||
false);
|
||||
}
|
||||
|
||||
void TTwAPI::sendAnnouncementToChat(const QString &message) {
|
||||
// Этот метод может использоваться для обычных сообщений в чат
|
||||
// Но для Twitch API это тот же endpoint
|
||||
sendAnnouncement(message);
|
||||
}
|
||||
|
||||
void TTwAPI::getGlobalChatBadges(QVector<ChatBadge> &badges)
|
||||
{
|
||||
QString response = getTTW("chat/badges/global", m_clientId, false);
|
||||
parseBadgesFromApi(response, badges);
|
||||
}
|
||||
|
||||
void TTwAPI::getCustomChatBadges(QVector<ChatBadge> &badges)
|
||||
{
|
||||
QString roomId = getRoomId();
|
||||
if (roomId.isEmpty()) {
|
||||
toLog(2, "TTwAPI::getCustomChatBadges", "Не удалось получить roomId");
|
||||
return;
|
||||
}
|
||||
QString response = getTTW("chat/badges?broadcaster_id=" + roomId, m_clientId, false);
|
||||
parseBadgesFromApi(response, badges);
|
||||
}
|
||||
|
||||
void TTwAPI::parseBadgesFromApi(const QString &jsonString, QVector<ChatBadge> &badges)
|
||||
{
|
||||
if (jsonString.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
toLog(2, "TTwAPI::parseBadgesFromApi", "Неверный JSON формат");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonArray data = root["data"].toArray();
|
||||
|
||||
for (const QJsonValue &value : data) {
|
||||
QJsonObject badgeObj = value.toObject();
|
||||
ChatBadge badge;
|
||||
badge.setId = badgeObj["set_id"].toString();
|
||||
|
||||
QJsonArray versions = badgeObj["versions"].toArray();
|
||||
for (const QJsonValue &versionValue : versions) {
|
||||
QJsonObject versionObj = versionValue.toObject();
|
||||
BadgeVersion version;
|
||||
version.id = versionObj["id"].toString();
|
||||
version.imageUrl1x = versionObj["image_url_1x"].toString();
|
||||
version.imageUrl2x = versionObj["image_url_2x"].toString();
|
||||
version.imageUrl4x = versionObj["image_url_4x"].toString();
|
||||
version.title = versionObj["title"].toString();
|
||||
version.description = versionObj["description"].toString();
|
||||
|
||||
badge.versions.append(version);
|
||||
}
|
||||
|
||||
badges.append(badge);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#ifndef TTW_API_H
|
||||
#define TTW_API_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QDate>
|
||||
#include <QVector>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include "user.h"
|
||||
#include "badge.h"
|
||||
|
||||
class TCustomReward;
|
||||
class TCustomRewardEvent;
|
||||
class ChatBadge;
|
||||
class Emote;
|
||||
|
||||
class TTwAPI : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TTwAPI(QObject *parent = nullptr);
|
||||
~TTwAPI();
|
||||
|
||||
// Инициализация
|
||||
void init(const QString &clientId,
|
||||
const QString &token,
|
||||
const QString &streamerToken,
|
||||
const QString &channel,
|
||||
const QString &botName);
|
||||
|
||||
// Основные методы
|
||||
void getTTWStat(const QString &channel,
|
||||
int &avgViewers,
|
||||
int &maxViewers,
|
||||
int &hoursWatched,
|
||||
int &followers,
|
||||
int &followersTotal);
|
||||
|
||||
void shoutouts(const QString &id);
|
||||
void sendNotify(const QString &text);
|
||||
void banUser(const QString &id);
|
||||
void banUserTime(const QString &id, int timeMinutes);
|
||||
void unbanUser(const QString &id);
|
||||
void warnUser(const QString &id);
|
||||
void raid(const QString &id);
|
||||
void unraid();
|
||||
|
||||
void setModerator(const QString &id);
|
||||
void delModerator(const QString &id);
|
||||
void setVIP(const QString &id);
|
||||
void delVIP(const QString &id);
|
||||
|
||||
// Custom Rewards
|
||||
void getCustomRewards(QVector<TCustomReward*> &rewards);
|
||||
TCustomReward* createCustomReward(const QString &title,
|
||||
const QString &cost,
|
||||
const QString &prompt = "",
|
||||
bool isUserInput = false);
|
||||
void updateCustomReward(TCustomReward* reward);
|
||||
void updateRedemptionStatus(TCustomRewardEvent* event);
|
||||
void deleteCustomReward(const QString &id);
|
||||
|
||||
// Пользователи
|
||||
User getUserByLogin(const QString &login);
|
||||
QDate getFollow(const QString &id);
|
||||
|
||||
// Эмоции и бейджи
|
||||
void getGlobalChatBadges(QVector<ChatBadge> &badges); // Изменено
|
||||
void getCustomChatBadges(QVector<ChatBadge> &badges); // Изменено
|
||||
void getChannelEmotes(QVector<Emote*> &emotes);
|
||||
void getGlobalEmotes(QVector<Emote*> &emotes);
|
||||
|
||||
// Валидация токена
|
||||
bool validateTwitchToken(const QString &tokenName,
|
||||
const QString &tokenValue,
|
||||
int &daysValid);
|
||||
|
||||
// Вспомогательные методы
|
||||
QString getRoomId();
|
||||
QString getRoomAndBot();
|
||||
|
||||
void sendAnnouncement(const QString &message);
|
||||
|
||||
// Сигналы
|
||||
void logMessage(int level, const QString &method, const QString &message);
|
||||
|
||||
private:
|
||||
// HTTP методы
|
||||
QString getTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
bool isStreamer = false);
|
||||
|
||||
QString deleteTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
bool isStreamer = false);
|
||||
|
||||
QString postTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
const QByteArray &data,
|
||||
bool isStreamer = false);
|
||||
|
||||
QString patchTTW(const QString &method,
|
||||
const QString &clientId,
|
||||
const QByteArray &data,
|
||||
bool isStreamer = false);
|
||||
|
||||
// Вспомогательные методы
|
||||
QString getFollowedAtFromJson(const QString &jsonString);
|
||||
QString sendRequest(const QString &url,
|
||||
const QString &method,
|
||||
const QByteArray &data = QByteArray(),
|
||||
const QString &token = "");
|
||||
// Парсинг бейджей
|
||||
void parseBadgesFromApi(const QString &jsonString, QVector<ChatBadge> &badges);
|
||||
|
||||
// Члены данных
|
||||
QString m_tokenApi;
|
||||
QString m_tokenApiStreamer;
|
||||
QString m_clientId;
|
||||
QString m_channelName;
|
||||
QString m_botName;
|
||||
|
||||
void sendAnnouncementToChat(const QString &message);
|
||||
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QString getBotId();
|
||||
// Логгирование
|
||||
void toLog(int level, const QString &method, const QString &message);
|
||||
|
||||
signals:
|
||||
void apiError(const QString &method, const QString &error);
|
||||
void tokenExpired(const QString &error);
|
||||
void rateLimitExceeded();
|
||||
};
|
||||
|
||||
#endif // TTW_API_H
|
||||
@@ -0,0 +1,78 @@
|
||||
#ifndef TTW_TYPES_H
|
||||
#define TTW_TYPES_H
|
||||
|
||||
#include "qdatetime.h"
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
|
||||
struct TCustomReward {
|
||||
QString id;
|
||||
QString title;
|
||||
QString prompt;
|
||||
int cost;
|
||||
QString backgroundColor;
|
||||
bool isEnabled;
|
||||
bool isUserInputRequired;
|
||||
bool isMaxPerStreamEnabled;
|
||||
int maxPerStream;
|
||||
bool isMaxPerUserPerStreamEnabled;
|
||||
int maxPerUserPerStream;
|
||||
bool isGlobalCooldownEnabled;
|
||||
int globalCooldownSeconds;
|
||||
bool isPaused;
|
||||
bool isInStock;
|
||||
bool shouldRedemptionsSkipRequestQueue;
|
||||
|
||||
TCustomReward()
|
||||
: cost(0)
|
||||
, isEnabled(true)
|
||||
, isUserInputRequired(false)
|
||||
, isMaxPerStreamEnabled(false)
|
||||
, maxPerStream(0)
|
||||
, isMaxPerUserPerStreamEnabled(false)
|
||||
, maxPerUserPerStream(0)
|
||||
, isGlobalCooldownEnabled(false)
|
||||
, globalCooldownSeconds(0)
|
||||
, isPaused(false)
|
||||
, isInStock(true)
|
||||
, shouldRedemptionsSkipRequestQueue(false)
|
||||
{}
|
||||
};
|
||||
|
||||
struct TCustomRewardEvent {
|
||||
QString id;
|
||||
QString rewardId;
|
||||
QString userId;
|
||||
QString userLogin;
|
||||
QString userDisplayName;
|
||||
QString userInput;
|
||||
QString status;
|
||||
QDateTime redeemedAt;
|
||||
|
||||
TCustomRewardEvent() {}
|
||||
};
|
||||
|
||||
struct ChatBadge {
|
||||
QString setId;
|
||||
QString versionId;
|
||||
QString title;
|
||||
QString description;
|
||||
QString smallImageUrl;
|
||||
QString mediumImageUrl;
|
||||
QString largeImageUrl;
|
||||
|
||||
ChatBadge() {}
|
||||
};
|
||||
|
||||
struct Emote {
|
||||
QString id;
|
||||
QString name;
|
||||
QString imageUrl;
|
||||
QString format;
|
||||
QString scale;
|
||||
QString themeMode;
|
||||
|
||||
Emote() {}
|
||||
};
|
||||
|
||||
#endif // TTW_TYPES_H
|
||||
@@ -0,0 +1,31 @@
|
||||
// twitchchatmessage.h - структура сообщения
|
||||
#ifndef TWITCHCHATMESSAGE_H
|
||||
#define TWITCHCHATMESSAGE_H
|
||||
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
|
||||
struct TwitchChatMessage {
|
||||
QString id; // ID сообщения
|
||||
QString channel; // Канал
|
||||
QString sender; // Отправитель
|
||||
QString message; // Текст сообщения
|
||||
QDateTime timestamp; // Время отправки
|
||||
QString userType; // Тип пользователя (mod, broadcaster и т.д.)
|
||||
bool isModerator; // Модератор
|
||||
bool isSubscriber; // Сабскрайбер
|
||||
bool isBroadcaster; // Вещатель (стример)
|
||||
QString badgeInfo; // Информация о бейджах
|
||||
QString color; // Цвет ника
|
||||
QString displayName; // Отображаемое имя
|
||||
QMap<QString, QString> emotes; // Эмоции
|
||||
QString roomId; // ID комнаты
|
||||
QString userId; // ID пользователя
|
||||
QString rawMessage; // Сырое сообщение
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(TwitchChatMessage)
|
||||
|
||||
#endif // TWITCHCHATMESSAGE_H
|
||||
@@ -0,0 +1,307 @@
|
||||
#include "twitchclient.h"
|
||||
#include <QDebug>
|
||||
#include <QCryptographicHash>
|
||||
#include <QRandomGenerator>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
|
||||
TwitchClient::TwitchClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_socket(nullptr)
|
||||
, m_webSocketHandshakeDone(false)
|
||||
, m_ircAuthenticated(false)
|
||||
, m_channelJoined(false)
|
||||
{
|
||||
m_writeTimer = new QTimer(this);
|
||||
m_writeTimer->setInterval(10); // 10ms для отправки данных
|
||||
connect(m_writeTimer, &QTimer::timeout, this, &TwitchClient::sendQueuedData);
|
||||
|
||||
m_pingTimer = new QTimer(this);
|
||||
m_pingTimer->setInterval(30000); // 30 секунд
|
||||
connect(m_pingTimer, &QTimer::timeout, this, &TwitchClient::sendPing);
|
||||
}
|
||||
|
||||
TwitchClient::~TwitchClient()
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void TwitchClient::connectToTwitch(const QString &oauthToken,
|
||||
const QString &nickname,
|
||||
const QString &channel)
|
||||
{
|
||||
if (m_socket) {
|
||||
disconnect();
|
||||
delete m_socket;
|
||||
}
|
||||
|
||||
m_socket = new QSslSocket(this);
|
||||
m_nickname = nickname;
|
||||
m_channel = channel;
|
||||
m_oauthToken = oauthToken;
|
||||
m_webSocketHandshakeDone = false;
|
||||
m_ircAuthenticated = false;
|
||||
m_channelJoined = false;
|
||||
m_receiveBuffer.clear();
|
||||
|
||||
// Подключаем сигналы
|
||||
connect(m_socket, &QSslSocket::connected, this, &TwitchClient::onConnected);
|
||||
connect(m_socket, &QSslSocket::readyRead, this, &TwitchClient::onReadyRead);
|
||||
connect(m_socket, &QSslSocket::disconnected, this, &TwitchClient::onDisconnected);
|
||||
connect(m_socket, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors),
|
||||
this, &TwitchClient::onSslErrors);
|
||||
connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QSslSocket::error),
|
||||
this, &TwitchClient::onSocketError);
|
||||
|
||||
// Настраиваем SSL
|
||||
m_socket->setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
|
||||
m_socket->connectToHostEncrypted("irc-ws.chat.twitch.tv", 443);
|
||||
}
|
||||
|
||||
void TwitchClient::onConnected()
|
||||
{
|
||||
// SSL handshake начинается автоматически
|
||||
}
|
||||
|
||||
void TwitchClient::onReadyRead()
|
||||
{
|
||||
m_receiveBuffer.append(m_socket->readAll());
|
||||
|
||||
if (!m_webSocketHandshakeDone) {
|
||||
// Обработка WebSocket handshake
|
||||
int headerEnd = m_receiveBuffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd != -1) {
|
||||
QString response = QString::fromUtf8(m_receiveBuffer.left(headerEnd));
|
||||
|
||||
if (response.contains("101 Switching Protocols")) {
|
||||
m_webSocketHandshakeDone = true;
|
||||
m_receiveBuffer = m_receiveBuffer.mid(headerEnd + 4);
|
||||
|
||||
// Запускаем таймеры
|
||||
m_writeTimer->start();
|
||||
m_pingTimer->start();
|
||||
|
||||
emit systemMessage("WebSocket connected");
|
||||
emit connected();
|
||||
|
||||
// Отправляем PING для проверки
|
||||
sendPing();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Обработка WebSocket фреймов
|
||||
while (m_receiveBuffer.size() >= 2) {
|
||||
// Простой парсинг WebSocket фреймов
|
||||
// Для продуктивного использования нужен полноценный парсер
|
||||
|
||||
// Ищем конец фрейма (предполагаем текстовые сообщения)
|
||||
QString data = QString::fromUtf8(m_receiveBuffer);
|
||||
QStringList lines = data.split("\r\n", Qt::SkipEmptyParts);
|
||||
|
||||
foreach (const QString &line, lines) {
|
||||
if (line.startsWith("PING")) {
|
||||
// Отвечаем на PING
|
||||
sendWebSocketFrame("PONG :tmi.twitch.tv");
|
||||
emit systemMessage("PING/PONG");
|
||||
} else if (!line.isEmpty() && line != "\n") {
|
||||
parseIrcMessage(line);
|
||||
}
|
||||
}
|
||||
|
||||
m_receiveBuffer.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchClient::sendWebSocketFrame(const QByteArray &data, quint8 opcode)
|
||||
{
|
||||
QByteArray frame = createWebSocketFrame(data, opcode);
|
||||
m_writeQueue.enqueue(frame);
|
||||
|
||||
if (!m_writeTimer->isActive()) {
|
||||
m_writeTimer->start();
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray TwitchClient::createWebSocketFrame(const QByteArray &data, quint8 opcode)
|
||||
{
|
||||
QByteArray frame;
|
||||
|
||||
// FIN = 1, opcode
|
||||
frame.append(static_cast<char>(0x80 | opcode));
|
||||
|
||||
// Маска и длина
|
||||
if (data.size() <= 125) {
|
||||
frame.append(static_cast<char>(0x80 | data.size()));
|
||||
} else if (data.size() <= 65535) {
|
||||
frame.append(static_cast<char>(0x80 | 126));
|
||||
frame.append(static_cast<char>((data.size() >> 8) & 0xFF));
|
||||
frame.append(static_cast<char>(data.size() & 0xFF));
|
||||
} else {
|
||||
frame.append(static_cast<char>(0x80 | 127));
|
||||
// Для больших размеров (не нужно для Twitch)
|
||||
}
|
||||
|
||||
// Маскирующий ключ (4 случайных байта)
|
||||
QByteArray mask(4, 0);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
mask[i] = QRandomGenerator::global()->generate() & 0xFF;
|
||||
}
|
||||
frame.append(mask);
|
||||
|
||||
// Маскированные данные
|
||||
QByteArray maskedData = data;
|
||||
for (int i = 0; i < maskedData.size(); ++i) {
|
||||
maskedData[i] = maskedData[i] ^ mask[i % 4];
|
||||
}
|
||||
frame.append(maskedData);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void TwitchClient::sendIrcCommand(const QString &command, const QString ¶ms)
|
||||
{
|
||||
QString message = command;
|
||||
if (!params.isEmpty()) {
|
||||
message += " " + params;
|
||||
}
|
||||
message += "\r\n";
|
||||
|
||||
sendWebSocketFrame(message.toUtf8());
|
||||
}
|
||||
|
||||
void TwitchClient::parseIrcMessage(const QString &message)
|
||||
{
|
||||
|
||||
if (message.startsWith("PING")) {
|
||||
sendWebSocketFrame("PONG :tmi.twitch.tv");
|
||||
}
|
||||
else if (message.contains("001")) {
|
||||
// Добро пожаловать сообщение
|
||||
m_ircAuthenticated = true;
|
||||
emit systemMessage("Authenticated with Twitch");
|
||||
|
||||
// Присоединяемся к каналу
|
||||
sendIrcCommand("JOIN", "#" + m_channel.toLower());
|
||||
}
|
||||
else if (message.contains("JOIN")) {
|
||||
if (message.contains("#" + m_channel.toLower())) {
|
||||
m_channelJoined = true;
|
||||
emit systemMessage("Joined channel: " + m_channel);
|
||||
}
|
||||
}
|
||||
else if (message.contains("PRIVMSG")) {
|
||||
// Парсим сообщение чата
|
||||
// Формат: :nick!nick@nick.tmi.twitch.tv PRIVMSG #channel :message
|
||||
QStringList parts = message.split(" ", Qt::SkipEmptyParts);
|
||||
if (parts.size() >= 4) {
|
||||
QString sender = parts[0].mid(1).split("!")[0];
|
||||
QString channel = parts[2];
|
||||
QString chatMessage = message.mid(message.indexOf(" :") + 2);
|
||||
|
||||
emit newMessage(sender, channel, chatMessage);
|
||||
}
|
||||
}
|
||||
else if (message.contains("NOTICE")) {
|
||||
emit systemMessage("NOTICE: " + message);
|
||||
}
|
||||
|
||||
emit systemMessage(message);
|
||||
}
|
||||
|
||||
void TwitchClient::onSslErrors(const QList<QSslError> &errors)
|
||||
{
|
||||
QStringList errorList;
|
||||
for (const QSslError &error : errors) {
|
||||
errorList.append(error.errorString());
|
||||
}
|
||||
|
||||
// Игнорируем SSL ошибки для разработки
|
||||
m_socket->ignoreSslErrors();
|
||||
|
||||
// После игнорирования ошибок, отправляем WebSocket handshake
|
||||
if (m_socket->isEncrypted()) {
|
||||
QString handshake = QString(
|
||||
"GET / HTTP/1.1\r\n"
|
||||
"Host: irc-ws.chat.twitch.tv\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %1\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n"
|
||||
).arg(QString(generateWebSocketKey()));
|
||||
|
||||
m_socket->write(handshake.toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchClient::onSocketError(QAbstractSocket::SocketError error)
|
||||
{
|
||||
QString errorMsg = QString("Socket error: %1 - %2")
|
||||
.arg(error)
|
||||
.arg(m_socket ? m_socket->errorString() : "Unknown");
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
void TwitchClient::onDisconnected()
|
||||
{
|
||||
m_writeTimer->stop();
|
||||
m_pingTimer->stop();
|
||||
emit disconnected();
|
||||
}
|
||||
|
||||
void TwitchClient::sendQueuedData()
|
||||
{
|
||||
if (!m_writeQueue.isEmpty() && m_socket && m_socket->state() == QAbstractSocket::ConnectedState) {
|
||||
QByteArray data = m_writeQueue.dequeue();
|
||||
m_socket->write(data);
|
||||
}
|
||||
|
||||
if (m_writeQueue.isEmpty()) {
|
||||
m_writeTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchClient::sendPing()
|
||||
{
|
||||
if (m_webSocketHandshakeDone) {
|
||||
sendWebSocketFrame("PING :tmi.twitch.tv");
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray TwitchClient::generateWebSocketKey()
|
||||
{
|
||||
QByteArray key(16, 0);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
key[i] = QRandomGenerator::global()->generate() & 0xFF;
|
||||
}
|
||||
return key.toBase64();
|
||||
}
|
||||
|
||||
void TwitchClient::sendMessage(const QString &message)
|
||||
{
|
||||
if (m_channelJoined) {
|
||||
sendIrcCommand("PRIVMSG", "#" + m_channel.toLower() + " :" + message);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchClient::disconnect()
|
||||
{
|
||||
if (m_socket) {
|
||||
if (m_channelJoined) {
|
||||
sendIrcCommand("PART", "#" + m_channel.toLower());
|
||||
}
|
||||
m_socket->disconnectFromHost();
|
||||
m_writeTimer->stop();
|
||||
m_pingTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool TwitchClient::isConnected() const
|
||||
{
|
||||
return m_socket && m_socket->state() == QAbstractSocket::ConnectedState && m_webSocketHandshakeDone;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#ifndef TWITCHCLIENT_H
|
||||
#define TWITCHCLIENT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSslSocket>
|
||||
#include <QTimer>
|
||||
#include <QByteArray>
|
||||
#include <QQueue>
|
||||
|
||||
class TwitchClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TwitchClient(QObject *parent = nullptr);
|
||||
~TwitchClient();
|
||||
|
||||
// Основные методы
|
||||
void connectToTwitch(const QString &oauthToken,
|
||||
const QString &nickname,
|
||||
const QString &channel);
|
||||
void sendMessage(const QString &message);
|
||||
void disconnect();
|
||||
bool isConnected() const;
|
||||
|
||||
signals:
|
||||
void newMessage(const QString &sender, const QString &channel, const QString &message);
|
||||
void systemMessage(const QString &message);
|
||||
void connected();
|
||||
void disconnected();
|
||||
void error(const QString &errorMessage);
|
||||
|
||||
private slots:
|
||||
void onConnected();
|
||||
void onReadyRead();
|
||||
void onSslErrors(const QList<QSslError> &errors);
|
||||
void onSocketError(QAbstractSocket::SocketError error);
|
||||
void onDisconnected();
|
||||
void sendQueuedData();
|
||||
void sendPing();
|
||||
|
||||
private:
|
||||
// WebSocket методы
|
||||
void sendWebSocketFrame(const QByteArray &data, quint8 opcode = 0x01);
|
||||
QByteArray createWebSocketFrame(const QByteArray &data, quint8 opcode);
|
||||
void processWebSocketFrame(const QByteArray &frame);
|
||||
|
||||
// IRC методы
|
||||
void sendIrcCommand(const QString &command, const QString ¶ms = "");
|
||||
void parseIrcMessage(const QString &message);
|
||||
|
||||
// Вспомогательные методы
|
||||
QByteArray generateWebSocketKey();
|
||||
QByteArray base64Encode(const QByteArray &data);
|
||||
QByteArray sha1Hash(const QByteArray &data);
|
||||
|
||||
private:
|
||||
QSslSocket *m_socket;
|
||||
QTimer *m_writeTimer;
|
||||
QTimer *m_pingTimer;
|
||||
QQueue<QByteArray> m_writeQueue;
|
||||
|
||||
QString m_nickname;
|
||||
QString m_channel;
|
||||
QString m_oauthToken;
|
||||
|
||||
bool m_webSocketHandshakeDone;
|
||||
bool m_ircAuthenticated;
|
||||
bool m_channelJoined;
|
||||
|
||||
QByteArray m_receiveBuffer;
|
||||
};
|
||||
|
||||
#endif // TWITCHCLIENT_H
|
||||
@@ -0,0 +1,117 @@
|
||||
// TwitchMessage.cpp
|
||||
#include "twitchmessage.h"
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
TwitchMessage TwitchMessage::parse(const QString &rawMessage)
|
||||
{
|
||||
TwitchMessage msg;
|
||||
|
||||
// Basic IRC parsing
|
||||
QString remaining = rawMessage.trimmed();
|
||||
|
||||
// Parse tags (start with @)
|
||||
if (remaining.startsWith('@')) {
|
||||
int spaceIndex = remaining.indexOf(' ');
|
||||
if (spaceIndex != -1) {
|
||||
QString tagsStr = remaining.mid(1, spaceIndex - 1);
|
||||
remaining = remaining.mid(spaceIndex + 1);
|
||||
|
||||
// Parse individual tags
|
||||
QStringList tags = tagsStr.split(';');
|
||||
for (const QString &tag : tags) {
|
||||
int eqIndex = tag.indexOf('=');
|
||||
if (eqIndex != -1) {
|
||||
QString key = tag.left(eqIndex);
|
||||
QString value = tag.mid(eqIndex + 1);
|
||||
|
||||
if (key == "badges") {
|
||||
if (!value.isEmpty()) {
|
||||
QStringList badgeList = value.split(',');
|
||||
for (const QString &badge : badgeList) {
|
||||
QStringList parts = badge.split('/');
|
||||
if (parts.size() == 2) {
|
||||
TwitchMessage::Badge b;
|
||||
b.name = parts[0];
|
||||
b.version = parts[1].toInt();
|
||||
msg.badges.append(b);
|
||||
if (b.name == "vip") {
|
||||
msg.isVIP = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key == "badge-info") {
|
||||
msg.badgeInfo = value;
|
||||
} else if (key == "client-nonce") {
|
||||
msg.clientNonce = value;
|
||||
} else if (key == "color") {
|
||||
msg.color = value;
|
||||
} else if (key == "display-name") {
|
||||
msg.displayName = value;
|
||||
} else if (key == "emotes") {
|
||||
msg.emotes = value;
|
||||
} else if (key == "first-msg") {
|
||||
msg.firstMsg = (value == "1");
|
||||
} else if (key == "flags") {
|
||||
msg.flags = value;
|
||||
} else if (key == "id") {
|
||||
msg.messageId = value;
|
||||
} else if (key == "mod") {
|
||||
msg.isMod = (value == "1");
|
||||
} else if (key == "vip") {
|
||||
msg.isVIP = (value == "1");
|
||||
} else if (key == "returning-chatter") {
|
||||
msg.returningChatter = (value == "1");
|
||||
} else if (key == "room-id") {
|
||||
msg.roomId = value.toLongLong();
|
||||
} else if (key == "subscriber") {
|
||||
msg.isSubscriber = (value == "1");
|
||||
} else if (key == "tmi-sent-ts") {
|
||||
msg.tmiSentTimestamp = value.toLongLong();
|
||||
} else if (key == "turbo") {
|
||||
msg.isTurbo = (value == "1");
|
||||
} else if (key == "user-id") {
|
||||
msg.userId = value.toLongLong();
|
||||
} else if (key == "user-type") {
|
||||
msg.userType = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse prefix (starts with : after tags)
|
||||
if (remaining.startsWith(':')) {
|
||||
int spaceIndex = remaining.indexOf(' ');
|
||||
if (spaceIndex != -1) {
|
||||
msg.ircPrefix = remaining.mid(1, spaceIndex - 1);
|
||||
remaining = remaining.mid(spaceIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command
|
||||
int spaceIndex = remaining.indexOf(' ');
|
||||
if (spaceIndex != -1) {
|
||||
msg.ircCommand = remaining.left(spaceIndex);
|
||||
remaining = remaining.mid(spaceIndex + 1);
|
||||
}
|
||||
|
||||
// Parse channel (starts with #)
|
||||
if (remaining.startsWith('#')) {
|
||||
spaceIndex = remaining.indexOf(' ');
|
||||
if (spaceIndex != -1) {
|
||||
msg.channel = remaining.left(spaceIndex);
|
||||
remaining = remaining.mid(spaceIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse message (starts with :)
|
||||
if (remaining.startsWith(':')) {
|
||||
msg.message = remaining.mid(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return msg;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// TwitchMessage.h
|
||||
#ifndef TWITCHMESSAGE_H
|
||||
#define TWITCHMESSAGE_H
|
||||
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QMap>
|
||||
|
||||
struct TwitchMessage
|
||||
{
|
||||
// Tags (после @)
|
||||
struct Badge {
|
||||
QString name;
|
||||
int version;
|
||||
};
|
||||
|
||||
QList<Badge> badges;
|
||||
QString badgeInfo;
|
||||
QString clientNonce;
|
||||
QString color;
|
||||
QString displayName;
|
||||
QString emotes;
|
||||
bool firstMsg;
|
||||
QString flags;
|
||||
QString messageId;
|
||||
bool isMod;
|
||||
bool isVIP;
|
||||
bool returningChatter;
|
||||
qint64 roomId;
|
||||
bool isSubscriber;
|
||||
qint64 tmiSentTimestamp;
|
||||
bool isTurbo;
|
||||
qint64 userId;
|
||||
QString userType;
|
||||
|
||||
// IRC parts
|
||||
QString ircPrefix;
|
||||
QString ircCommand;
|
||||
QString channel;
|
||||
QString message;
|
||||
|
||||
// Timestamp as QDateTime
|
||||
QDateTime timestamp() const {
|
||||
return QDateTime::fromMSecsSinceEpoch(tmiSentTimestamp);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
TwitchMessage()
|
||||
: firstMsg(false)
|
||||
, isMod(false)
|
||||
, isVIP(false)
|
||||
, returningChatter(false)
|
||||
, roomId(0)
|
||||
, isSubscriber(false)
|
||||
, tmiSentTimestamp(0)
|
||||
, isTurbo(false)
|
||||
, userId(0)
|
||||
{}
|
||||
|
||||
// Parse from raw IRC message
|
||||
static TwitchMessage parse(const QString &rawMessage);
|
||||
};
|
||||
|
||||
#endif // TWITCHMESSAGE_H
|
||||
@@ -0,0 +1,928 @@
|
||||
#include "udatabase.h"
|
||||
#include <QMetaProperty>
|
||||
#include <QSqlRecord>
|
||||
#include <QDateTime>
|
||||
#include <QTableWidget>
|
||||
|
||||
#include "qlistwidget.h"
|
||||
|
||||
uDataBase::uDataBase(const QString& dbFileName, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_dbFileName(dbFileName)
|
||||
{
|
||||
// Создаем уникальное имя подключения
|
||||
QString connectionName = QString("settings_connection_%1").arg((quintptr)this);
|
||||
|
||||
// Открываем базу данных
|
||||
m_db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||
m_db.setDatabaseName(m_dbFileName);
|
||||
|
||||
if (!m_db.open()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
qWarning() << "Failed to open database:" << m_lastError;
|
||||
return;
|
||||
}
|
||||
|
||||
// Инициализируем базу данных
|
||||
if (!initializeDatabase()) {
|
||||
qWarning() << "Failed to initialize database";
|
||||
}
|
||||
}
|
||||
|
||||
uDataBase::~uDataBase()
|
||||
{
|
||||
if (m_db.isOpen()) {
|
||||
QString connectionName = m_db.connectionName();
|
||||
m_db.close();
|
||||
m_db = QSqlDatabase(); // сбрасываем объект базы данных
|
||||
QSqlDatabase::removeDatabase(connectionName); // удаляем соединение
|
||||
}
|
||||
}
|
||||
|
||||
bool uDataBase::close()
|
||||
{
|
||||
if (!isConnected()) {
|
||||
return true; // Уже закрыто
|
||||
}
|
||||
|
||||
// Сохраняем имя соединения перед закрытием
|
||||
QString connectionName = m_db.connectionName();
|
||||
|
||||
// Закрываем базу данных
|
||||
m_db.close();
|
||||
|
||||
// Очищаем объект базы данных
|
||||
m_db = QSqlDatabase();
|
||||
|
||||
// Удаляем соединение из пула Qt
|
||||
QSqlDatabase::removeDatabase(connectionName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::initializeDatabase()
|
||||
{
|
||||
// Создаем таблицу настроек, если она не существует
|
||||
QString createTableQuery =
|
||||
"CREATE TABLE IF NOT EXISTS params ("
|
||||
"name TEXT PRIMARY KEY,"
|
||||
"value TEXT"
|
||||
")";
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
if (!query.exec(createTableQuery)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to create table:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString uDataBase::readSetting(const QString& aName, const QString& aDefault)
|
||||
{
|
||||
if (!m_db.isOpen()) {
|
||||
qWarning() << "Database is not open";
|
||||
return aDefault;
|
||||
}
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT value FROM params WHERE name = :name");
|
||||
query.bindValue(":name", aName);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to read setting" << aName << ":" << m_lastError;
|
||||
return aDefault;
|
||||
}
|
||||
|
||||
if (query.next()) {
|
||||
return query.value(0).toString();
|
||||
}
|
||||
|
||||
// Если настройка не найдена, возвращаем значение по умолчанию
|
||||
return aDefault;
|
||||
}
|
||||
|
||||
bool uDataBase::writeSetting(const QString& aName, const QString& aValue)
|
||||
{
|
||||
if (!m_db.isOpen()) {
|
||||
qWarning() << "Database is not open";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Используем INSERT OR REPLACE для обновления существующей записи или создания новой
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare(
|
||||
"INSERT OR REPLACE INTO params (name, value) "
|
||||
"VALUES (:name, :value)"
|
||||
);
|
||||
query.bindValue(":name", aName);
|
||||
query.bindValue(":value", aValue);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to write setting" << aName << ":" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// В классе uDataBase добавьте эти методы:
|
||||
|
||||
/**
|
||||
* @brief Специальная загрузка для таблицы таймеров с сохранением состояния
|
||||
* @param tableWidget Таблица для загрузки
|
||||
* @param timers Список таймеров для заполнения
|
||||
* @return true если успешно
|
||||
*/
|
||||
bool uDataBase::LoadTimers(QTableWidget *tableWidget, QList<TimerInfo> &timers)
|
||||
{
|
||||
if (!tableWidget) {
|
||||
m_lastError = "Table widget is null";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем имя таблицы из objectName виджета
|
||||
QString tableName = tableWidget->objectName();
|
||||
if (tableName.isEmpty()) {
|
||||
m_lastError = "Table widget has no object name";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем соединение с БД
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Очищаем таблицу и список таймеров
|
||||
tableWidget->setRowCount(0);
|
||||
timers.clear();
|
||||
|
||||
// Выполняем SQL запрос
|
||||
QSqlQuery query(m_db);
|
||||
QString sql = QString("SELECT * FROM %1 ORDER BY id").arg(tableName);
|
||||
|
||||
if (!query.exec(sql)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Query failed:" << m_lastError;
|
||||
qWarning() << "SQL:" << sql;
|
||||
return false;
|
||||
}
|
||||
|
||||
int rowCount = 0;
|
||||
while (query.next()) {
|
||||
// Создаем объект таймера
|
||||
TimerInfo timer;
|
||||
timer.id = query.value("id").toInt();
|
||||
timer.message = query.value("message").toString();
|
||||
timer.interval = query.value("interval").toInt();
|
||||
timer.isActive = query.value("is_active").toBool();
|
||||
timer.isAnnouncement = query.value("is_announcement").toBool();
|
||||
timer.timer = nullptr;
|
||||
|
||||
// Добавляем в список
|
||||
timers.append(timer);
|
||||
|
||||
// Добавляем строку в таблицу
|
||||
int row = tableWidget->rowCount();
|
||||
tableWidget->insertRow(row);
|
||||
|
||||
// Чекбокс "Вкл"
|
||||
QTableWidgetItem* enabledItem = new QTableWidgetItem();
|
||||
enabledItem->setCheckState(timer.isActive ? Qt::Checked : Qt::Unchecked);
|
||||
tableWidget->setItem(row, 0, enabledItem);
|
||||
|
||||
// Сообщение
|
||||
QTableWidgetItem* messageItem = new QTableWidgetItem(timer.message);
|
||||
tableWidget->setItem(row, 1, messageItem);
|
||||
|
||||
// Интервал
|
||||
QTableWidgetItem* intervalItem = new QTableWidgetItem(QString::number(timer.interval));
|
||||
tableWidget->setItem(row, 2, intervalItem);
|
||||
|
||||
// Чекбокс "О"
|
||||
QTableWidgetItem* announcementItem = new QTableWidgetItem();
|
||||
announcementItem->setCheckState(timer.isAnnouncement ? Qt::Checked : Qt::Unchecked);
|
||||
tableWidget->setItem(row, 3, announcementItem);
|
||||
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Специальное сохранение для таблицы таймеров
|
||||
* @param tableWidget Таблица для сохранения
|
||||
* @param timers Список таймеров
|
||||
* @return true если успешно
|
||||
*/
|
||||
bool uDataBase::SaveTimers(QTableWidget *tableWidget, const QList<TimerInfo> &timers)
|
||||
{
|
||||
if (!tableWidget) {
|
||||
m_lastError = "Table widget is null";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем имя таблицы из objectName виджета
|
||||
QString tableName = tableWidget->objectName();
|
||||
if (tableName.isEmpty()) {
|
||||
m_lastError = "Table widget has no object name";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем соединение с БД
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаем таблицу если не существует
|
||||
QString createTableQuery = QString(
|
||||
"CREATE TABLE IF NOT EXISTS %1 ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"message TEXT NOT NULL,"
|
||||
"interval INTEGER NOT NULL,"
|
||||
"is_active BOOLEAN DEFAULT 1,"
|
||||
"is_announcement BOOLEAN DEFAULT 0"
|
||||
")"
|
||||
).arg(tableName);
|
||||
|
||||
QSqlQuery createQuery(m_db);
|
||||
if (!createQuery.exec(createTableQuery)) {
|
||||
m_lastError = createQuery.lastError().text();
|
||||
qWarning() << "Failed to create table:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Начинаем транзакцию
|
||||
if (!m_db.transaction()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
qWarning() << "Failed to start transaction:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Очищаем таблицу
|
||||
QSqlQuery clearQuery(m_db);
|
||||
QString clearSql = QString("DELETE FROM %1").arg(tableName);
|
||||
if (!clearQuery.exec(clearSql)) {
|
||||
m_lastError = clearQuery.lastError().text();
|
||||
qWarning() << "Failed to clear table:" << m_lastError;
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Сохраняем таймеры
|
||||
QSqlQuery insertQuery(m_db);
|
||||
insertQuery.prepare(QString(
|
||||
"INSERT INTO %1 (message, interval, is_active, is_announcement) "
|
||||
"VALUES (:message, :interval, :is_active, :is_announcement)"
|
||||
).arg(tableName));
|
||||
|
||||
int savedRows = 0;
|
||||
for (const TimerInfo &timer : timers) {
|
||||
insertQuery.bindValue(":message", timer.message);
|
||||
insertQuery.bindValue(":interval", timer.interval);
|
||||
insertQuery.bindValue(":is_active", timer.isActive);
|
||||
insertQuery.bindValue(":is_announcement", timer.isAnnouncement);
|
||||
|
||||
if (!insertQuery.exec()) {
|
||||
m_lastError = insertQuery.lastError().text();
|
||||
qWarning() << "Failed to insert timer:" << m_lastError;
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
savedRows++;
|
||||
}
|
||||
|
||||
// Завершаем транзакцию
|
||||
if (!m_db.commit()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
qWarning() << "Failed to commit transaction:" << m_lastError;
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "Saved" << savedRows << "timers to database";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::LoadTableWidget(QTableWidget *tableWidget)
|
||||
{
|
||||
if (!tableWidget) {
|
||||
m_lastError = "Table widget is null";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем имя таблицы из objectName виджета
|
||||
QString tableName = tableWidget->objectName();
|
||||
if (tableName.isEmpty()) {
|
||||
m_lastError = "Table widget has no object name";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем соединение с БД
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Очищаем таблицу (но сохраняем заголовки)
|
||||
tableWidget->setRowCount(0);
|
||||
|
||||
// Выполняем SQL запрос
|
||||
QSqlQuery query(m_db);
|
||||
QString sql = QString("SELECT * FROM %1 ORDER BY id").arg(tableName);
|
||||
|
||||
if (!query.exec(sql)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Query failed:" << m_lastError;
|
||||
qWarning() << "SQL:" << sql;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем информацию о колонках в результате запроса
|
||||
QSqlRecord record = query.record();
|
||||
int dbColumnCount = record.count();
|
||||
|
||||
// Определяем сколько колонок нам нужно в QTableWidget
|
||||
int widgetColumnCount = tableWidget->columnCount();
|
||||
int columnsToLoad = qMin(dbColumnCount - 1, widgetColumnCount); // -1 потому что пропускаем id
|
||||
|
||||
int rowCount = 0;
|
||||
while (query.next()) {
|
||||
// Добавляем новую строку
|
||||
int row = tableWidget->rowCount();
|
||||
tableWidget->insertRow(row);
|
||||
|
||||
// Заполняем ячейки (начиная с col0, пропускаем id)
|
||||
for (int col = 0; col < columnsToLoad; col++) {
|
||||
QString columnName = QString("col%1").arg(col);
|
||||
QVariant value = query.value(columnName);
|
||||
|
||||
QTableWidgetItem *item = new QTableWidgetItem();
|
||||
if (value.isNull()) {
|
||||
item->setText("");
|
||||
} else {
|
||||
item->setText(value.toString());
|
||||
}
|
||||
|
||||
// Устанавливаем выравнивание по умолчанию
|
||||
item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
tableWidget->setItem(row, col, item);
|
||||
}
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::SaveTableWidget(QTableWidget *tableWidget)
|
||||
{
|
||||
if (!tableWidget) {
|
||||
m_lastError = "Table widget is null";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем имя таблицы из objectName виджета
|
||||
QString tableName = tableWidget->objectName();
|
||||
if (tableName.isEmpty()) {
|
||||
m_lastError = "Table widget has no object name";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем соединение с БД
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем существует ли таблица
|
||||
if (!tableExists(tableName)) {
|
||||
// Создаем таблицу с нужным количеством колонок
|
||||
if (!createTableForWidget(tableName, tableWidget->columnCount())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Очищаем существующую таблицу
|
||||
if (!clearTable(tableName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем данные из таблицы
|
||||
QSqlQuery query(m_db);
|
||||
|
||||
// Начинаем транзакцию для быстрой вставки
|
||||
if (!m_db.transaction()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
qWarning() << "Failed to start transaction:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
int rowCount = tableWidget->rowCount();
|
||||
int colCount = tableWidget->columnCount();
|
||||
|
||||
if (colCount == 0) {
|
||||
m_lastError = "Table widget has no columns";
|
||||
qWarning() << m_lastError;
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Подготавливаем SQL запрос для вставки
|
||||
QStringList columnNames;
|
||||
QStringList valuePlaceholders;
|
||||
|
||||
// Добавляем id (автоинкремент)
|
||||
columnNames << "id";
|
||||
valuePlaceholders << "NULL";
|
||||
|
||||
// Добавляем col0, col1, col2 и т.д.
|
||||
for (int col = 0; col < colCount; col++) {
|
||||
columnNames << QString("col%1").arg(col);
|
||||
valuePlaceholders << QString(":col%1").arg(col);
|
||||
}
|
||||
|
||||
QString sql = QString("INSERT INTO %1 (%2) VALUES (%3)")
|
||||
.arg(tableName)
|
||||
.arg(columnNames.join(", "))
|
||||
.arg(valuePlaceholders.join(", "));
|
||||
|
||||
query.prepare(sql);
|
||||
|
||||
int savedRows = 0;
|
||||
for (int row = 0; row < rowCount; row++) {
|
||||
// Пропускаем пустые строки (если все ячейки пустые)
|
||||
bool isEmptyRow = true;
|
||||
for (int col = 0; col < colCount; col++) {
|
||||
QTableWidgetItem *item = tableWidget->item(row, col);
|
||||
if (item && !item->text().trimmed().isEmpty()) {
|
||||
isEmptyRow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmptyRow) {
|
||||
continue; // Пропускаем полностью пустые строки
|
||||
}
|
||||
|
||||
// Биндим значения для каждой колонки
|
||||
for (int col = 0; col < colCount; col++) {
|
||||
QString paramName = QString(":col%1").arg(col);
|
||||
QTableWidgetItem *item = tableWidget->item(row, col);
|
||||
|
||||
if (item && !item->text().isEmpty()) {
|
||||
query.bindValue(paramName, item->text());
|
||||
} else {
|
||||
query.bindValue(paramName, QVariant(QVariant::String));
|
||||
}
|
||||
}
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to insert row:" << m_lastError;
|
||||
qWarning() << "SQL:" << sql;
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
savedRows++;
|
||||
}
|
||||
|
||||
// Завершаем транзакцию
|
||||
if (!m_db.commit()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
qWarning() << "Failed to commit transaction:" << m_lastError;
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
bool uDataBase::tableExists(const QString &tableName)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
QString sql = QString("SELECT name FROM sqlite_master WHERE type='table' AND name='%1'")
|
||||
.arg(tableName);
|
||||
|
||||
if (query.exec(sql) && query.next()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool uDataBase::createTableForWidget(const QString &tableName, int columnCount)
|
||||
{
|
||||
if (columnCount <= 0) {
|
||||
m_lastError = "Invalid column count";
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
|
||||
// Создаем SQL для создания таблицы
|
||||
QStringList columns;
|
||||
columns << "id INTEGER PRIMARY KEY AUTOINCREMENT";
|
||||
|
||||
for (int i = 0; i < columnCount; i++) {
|
||||
columns << QString("col%1 TEXT").arg(i);
|
||||
}
|
||||
|
||||
QString sql = QString("CREATE TABLE %1 (%2)").arg(tableName).arg(columns.join(", "));
|
||||
|
||||
if (!query.exec(sql)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to create table:" << m_lastError;
|
||||
qWarning() << "SQL:" << sql;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::clearTable(const QString &tableName)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
QString sql = QString("DELETE FROM %1").arg(tableName);
|
||||
|
||||
if (!query.exec(sql)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to clear table:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Сбрасываем автоинкремент
|
||||
sql = QString("DELETE FROM sqlite_sequence WHERE name='%1'").arg(tableName);
|
||||
query.exec(sql); // Игнорируем ошибки, т.к. может не быть sqlite_sequence
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool uDataBase::LoadRandomGroups(QListWidget *listWidget)
|
||||
{
|
||||
if (!listWidget) {
|
||||
m_lastError = "List widget is null";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
listWidget->clear();
|
||||
m_lastError.clear();
|
||||
|
||||
// Проверяем соединение с БД
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем существование таблицы
|
||||
if (!tableExists("GroupResponse")) {
|
||||
qWarning() << "Table GroupResponse doesn't exist";
|
||||
// Можно создать таблицу автоматически
|
||||
if (!createGroupResponseTable()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Выполняем SQL запрос для получения уникальных имен
|
||||
QSqlQuery query(m_db);
|
||||
QString sql = "SELECT DISTINCT Name FROM GroupResponse ORDER BY Name";
|
||||
|
||||
if (!query.exec(sql)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Query failed:" << m_lastError;
|
||||
qWarning() << "SQL:" << sql;
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while (query.next()) {
|
||||
QString groupName = query.value(0).toString();
|
||||
if (!groupName.isEmpty()) {
|
||||
listWidget->addItem(groupName);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Метод для загрузки ответов конкретной группы
|
||||
bool uDataBase::LoadRandomResponses(const QString &groupName, QListWidget *listWidget)
|
||||
{
|
||||
if (!listWidget) {
|
||||
m_lastError = "List widget is null";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (groupName.isEmpty()) {
|
||||
m_lastError = "Group name is empty";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
listWidget->clear();
|
||||
m_lastError.clear();
|
||||
|
||||
// Проверяем соединение с БД
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем существование таблицы
|
||||
if (!tableExists("GroupResponse")) {
|
||||
m_lastError = "Table GroupResponse doesn't exist";
|
||||
qWarning() << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Выполняем SQL запрос для получения ответов группы
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT Response FROM GroupResponse WHERE Name = :name ORDER BY id");
|
||||
query.bindValue(":name", groupName);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Query failed:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while (query.next()) {
|
||||
QString response = query.value(0).toString();
|
||||
listWidget->addItem(response);
|
||||
count++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Метод для получения случайного ответа из группы
|
||||
QString uDataBase::GetRandomResponse(const QString &groupName)
|
||||
{
|
||||
if (groupName.isEmpty()) {
|
||||
qWarning() << "Group name is empty";
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (!m_db.isOpen()) {
|
||||
qWarning() << "Database is not open";
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (!tableExists("GroupResponse")) {
|
||||
qWarning() << "Table GroupResponse doesn't exist";
|
||||
return QString();
|
||||
}
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT Response FROM GroupResponse WHERE Name = :name ORDER BY RANDOM() LIMIT 1");
|
||||
query.bindValue(":name", groupName);
|
||||
|
||||
if (!query.exec()) {
|
||||
qWarning() << "Query failed for group:" << groupName << "Error:" << query.lastError().text();
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (!query.next()) {
|
||||
qWarning() << "No responses found for group:" << groupName;
|
||||
return QString();
|
||||
}
|
||||
|
||||
return query.value(0).toString();
|
||||
}
|
||||
// Метод для обработки текста с заменой {{grN}}
|
||||
QString uDataBase::ProcessResponseTemplate(const QString &templateText)
|
||||
{
|
||||
QRegularExpression re("\\{\\{(.+?)\\}\\}");
|
||||
QRegularExpressionMatchIterator i = re.globalMatch(templateText);
|
||||
|
||||
QString result = templateText;
|
||||
|
||||
while (i.hasNext()) {
|
||||
QRegularExpressionMatch match = i.next();
|
||||
QString fullMatch = match.captured(0); // {{ANY_GROUP_NAME}}
|
||||
QString groupName = match.captured(1).trimmed(); // ANY_GROUP_NAME
|
||||
|
||||
if (groupName.isEmpty()) {
|
||||
continue; // Пропускаем пустые имена групп
|
||||
}
|
||||
|
||||
QString replacement = GetRandomResponse(groupName);
|
||||
|
||||
if (replacement.isEmpty()) {
|
||||
qWarning() << "No response found for group:" << groupName;
|
||||
// Можно оставить оригинальный текст или заменить на что-то другое
|
||||
replacement = QString("[Группа '%1' не найдена]").arg(groupName);
|
||||
}
|
||||
|
||||
result.replace(fullMatch, replacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
// Методы для добавления/удаления групп и ответов
|
||||
bool uDataBase::AddGroupResponse(const QString &groupName, const QString &response)
|
||||
{
|
||||
if (groupName.isEmpty() || response.isEmpty()) {
|
||||
m_lastError = "Group name or response is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаем таблицу если она не существует
|
||||
if (!tableExists("GroupResponse")) {
|
||||
if (!createGroupResponseTable()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("INSERT INTO GroupResponse (Name, Response) VALUES (:name, :response)");
|
||||
query.bindValue(":name", groupName);
|
||||
query.bindValue(":response", response);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to add response:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::DeleteGroup(const QString &groupName)
|
||||
{
|
||||
if (groupName.isEmpty()) {
|
||||
m_lastError = "Group name is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("DELETE FROM GroupResponse WHERE Name = :name");
|
||||
query.bindValue(":name", groupName);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to delete group:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::DeleteResponse(const QString &groupName, const QString &ResponseName)
|
||||
{
|
||||
if (groupName.isEmpty()) {
|
||||
m_lastError = "Group name is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем ID ответа по индексу в группе
|
||||
QSqlQuery query(m_db);
|
||||
|
||||
// Удаляем ответ по ID
|
||||
query.prepare("DELETE FROM GroupResponse WHERE Name = :name and Response = :response");
|
||||
query.bindValue(":name", groupName);
|
||||
query.bindValue(":response", ResponseName);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to delete response:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Вспомогательные методы
|
||||
bool uDataBase::createGroupResponseTable()
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
QString sql =
|
||||
"CREATE TABLE IF NOT EXISTS GroupResponse ("
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
" Name TEXT NOT NULL,"
|
||||
" Response TEXT NOT NULL"
|
||||
")";
|
||||
|
||||
if (!query.exec(sql)) {
|
||||
m_lastError = query.lastError().text();
|
||||
qWarning() << "Failed to create GroupResponse table:" << m_lastError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаем индекс для быстрого поиска по имени группы
|
||||
sql = "CREATE INDEX IF NOT EXISTS idx_groupresponse_name ON GroupResponse (Name)";
|
||||
query.exec(sql); // Игнорируем ошибки, если индекс уже существует
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Метод для сохранения списка ответов группы (заменяет все существующие)
|
||||
bool uDataBase::SaveGroupResponses(const QString &groupName, const QStringList &responses)
|
||||
{
|
||||
if (groupName.isEmpty()) {
|
||||
m_lastError = "Group name is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_db.isOpen()) {
|
||||
m_lastError = "Database is not open";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Начинаем транзакцию
|
||||
if (!m_db.transaction()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Удаляем старые ответы группы
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("DELETE FROM GroupResponse WHERE Name = :name");
|
||||
query.bindValue(":name", groupName);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Добавляем новые ответы
|
||||
query.prepare("INSERT INTO GroupResponse (Name, Response) VALUES (:name, :response)");
|
||||
|
||||
foreach (const QString &response, responses) {
|
||||
if (!response.trimmed().isEmpty()) {
|
||||
query.bindValue(":name", groupName);
|
||||
query.bindValue(":response", response);
|
||||
|
||||
if (!query.exec()) {
|
||||
m_lastError = query.lastError().text();
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_db.commit()) {
|
||||
m_lastError = m_db.lastError().text();
|
||||
m_db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uDataBase::isConnected() const
|
||||
{
|
||||
return m_db.isOpen();
|
||||
}
|
||||
|
||||
QString uDataBase::lastError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef UDATABASE_H
|
||||
#define UDATABASE_H
|
||||
|
||||
#include "qlistwidget.h"
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QVariant>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QTableWidget>
|
||||
#include "timerinfo.h"
|
||||
|
||||
class uDataBase : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
// Информация о таймере
|
||||
|
||||
public:
|
||||
// Конструктор с указанием файла базы данных
|
||||
explicit uDataBase(const QString& dbFileName, QObject* parent = nullptr);
|
||||
|
||||
// Деструктор
|
||||
~uDataBase();
|
||||
|
||||
// Чтение настройки
|
||||
QString readSetting(const QString& aName, const QString& aDefault = "");
|
||||
|
||||
// Запись настройки
|
||||
bool writeSetting(const QString& aName, const QString& aValue);
|
||||
|
||||
|
||||
bool LoadTableWidget(QTableWidget *tableWidget);
|
||||
bool clearTable(const QString &tableName);
|
||||
bool createTableForWidget(const QString &tableName, int columnCount);
|
||||
bool tableExists(const QString &tableName);
|
||||
bool SaveTableWidget(QTableWidget *tableWidget);
|
||||
bool SaveTimers(QTableWidget *tableWidget, const QList<TimerInfo> &timers);
|
||||
bool LoadTimers(QTableWidget *tableWidget, QList<TimerInfo> &timers);
|
||||
bool SaveGroupResponses(const QString &groupName, const QStringList &responses);
|
||||
bool createGroupResponseTable();
|
||||
bool DeleteResponse(const QString &groupName, const QString &ResponseName);
|
||||
bool DeleteGroup(const QString &groupName);
|
||||
bool AddGroupResponse(const QString &groupName, const QString &response);
|
||||
QString ProcessResponseTemplate(const QString &templateText);
|
||||
QString GetRandomResponse(const QString &groupName);
|
||||
bool LoadRandomResponses(const QString &groupName, QListWidget *listWidget);
|
||||
bool LoadRandomGroups(QListWidget *listWidget);
|
||||
QString ProcessResponseTemplateRecursive(const QString &templateText, int depth);
|
||||
|
||||
// Проверка подключения к БД
|
||||
bool isConnected() const;
|
||||
|
||||
bool close();
|
||||
// Получение последней ошибки
|
||||
QString lastError() const;
|
||||
|
||||
private:
|
||||
|
||||
|
||||
// Инициализация базы данных (создание таблицы, если нужно)
|
||||
bool initializeDatabase();
|
||||
|
||||
QSqlDatabase m_db;
|
||||
QString m_dbFileName;
|
||||
QString m_lastError;
|
||||
};
|
||||
|
||||
#endif // DBSETTINGS_H
|
||||
@@ -0,0 +1,485 @@
|
||||
#ifndef UGENERAL_H
|
||||
#define UGENERAL_H
|
||||
|
||||
// ============================================================================
|
||||
// ВКЛЮЧЕНИЕ ЗАГОЛОВОЧНЫХ ФАЙЛОВ
|
||||
// ============================================================================
|
||||
#include <QMainWindow>
|
||||
#include <tauth.h>
|
||||
#include <ulink.h>
|
||||
#include <udatabase.h>
|
||||
#include <soundmanager.h>
|
||||
#include "fcreatechat.h"
|
||||
#include "fcreatenotify.h"
|
||||
#include "neuralnetworkmanager.h"
|
||||
#include "ttw_api.h"
|
||||
#include "user_manager.h"
|
||||
#include "webserverchat.h"
|
||||
#include "webservernotify.h"
|
||||
#include "websocketclient.h"
|
||||
#include "userwidget.h"
|
||||
#include <QTimer>
|
||||
#include "emoteprovider.h"
|
||||
#include "timerinfo.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui { class uGeneral; }
|
||||
QT_END_NAMESPACE
|
||||
|
||||
// Структура для отправки сообщений
|
||||
struct MessageData {
|
||||
QString nickname;
|
||||
QString content;
|
||||
QString eventType; // Для уведомлений
|
||||
QVariantMap styleSettings; // Настройки стилей
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// КЛАСС UGeneral - ГЛАВНОЕ ОКНО ПРИЛОЖЕНИЯ
|
||||
// ============================================================================
|
||||
class uGeneral : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
// Переопределение обработчика закрытия окна
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
public:
|
||||
// Конструктор и деструктор
|
||||
uGeneral(QWidget *parent = nullptr);
|
||||
~uGeneral();
|
||||
|
||||
// ========================================================================
|
||||
// СТРУКТУРЫ ДАННЫХ
|
||||
// ========================================================================
|
||||
|
||||
// Данные для авторизации Twitch
|
||||
struct TTTVAuth {
|
||||
QString botName;
|
||||
QString botToken;
|
||||
QString strimerToken;
|
||||
QString ClientID;
|
||||
QString channelName;
|
||||
};
|
||||
|
||||
// Структура для хранения пар команд (вопрос-ответ)
|
||||
struct RListCommands {
|
||||
QString R1;
|
||||
QString R2;
|
||||
RListCommands() = default;
|
||||
RListCommands(const QString &r1, const QString &r2) : R1(r1), R2(r2) {}
|
||||
};
|
||||
|
||||
// Запись лога
|
||||
struct LogEntry {
|
||||
QDateTime time;
|
||||
QString module;
|
||||
QString method;
|
||||
QString message;
|
||||
int code;
|
||||
QString msgType;
|
||||
QColor color;
|
||||
};
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// ПУБЛИЧНЫЕ ЧЛЕНЫ КЛАССА
|
||||
// ========================================================================
|
||||
QList<RListCommands> TCommends; // Список команд
|
||||
BTTVProvider bttvProvider;
|
||||
SevenTVProvider sevenTVProvider;
|
||||
uDataBase *db; // Указатель на базу данных
|
||||
SoundManager *soundManager; // Менеджер звуков
|
||||
UserManager* getUserManager(); // Получение менеджера пользователей
|
||||
TTwAPI *twitchAPI; // API для работы с Twitch
|
||||
|
||||
// Методы логирования и работы с командами
|
||||
void toLog(QString aModule, QString aMethod, QString aMessage, int aCode);
|
||||
void toCommands(QString command, QString response);
|
||||
|
||||
private slots:
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ТАЙМЕРАМИ
|
||||
// ========================================================================
|
||||
void on_btnTimerAdd_clicked();
|
||||
void on_btnTimerEdit_clicked();
|
||||
void on_btnTimerDelete_clicked();
|
||||
void on_btnTimerTest_clicked();
|
||||
void on_sgTimers_cellClicked(int row, int column);
|
||||
void on_sgTimers_cellDoubleClicked(int row, int column);
|
||||
void onTimerTimeout(int timerId);
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ АВТОРИЗАЦИИ И НАСТРОЕК
|
||||
// ========================================================================
|
||||
void on_btnGetTockenBot_clicked(); // Получение токена бота
|
||||
void on_btnOpenFolderSettings_clicked(); // Открытие папки настроек
|
||||
void on_btnGetTokenStreamer_clicked(); // Получение токена стримера
|
||||
void on_btnDAGetCode_clicked(); // Получение кода DA
|
||||
|
||||
// Обработчики получения токенов
|
||||
void onTokenReceived(const QString &token);
|
||||
void onTokenReceived2(const QString &token);
|
||||
void onTokenReceived3(const QString &token);
|
||||
void onAuthError(const QString &error); // Ошибка авторизации
|
||||
|
||||
// Работа с настройками
|
||||
void loadSettings(); // Загрузка настроек
|
||||
void on_btnImportSettings_clicked(); // Импорт настроек
|
||||
void on_btnExportSettings_clicked(); // Экспорт настроек
|
||||
void on_btnSaveSettings_clicked(); // Сохранение настроек
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С УВЕДОМЛЕНИЯМИ
|
||||
// ========================================================================
|
||||
void on_btnNotifyCheck_clicked(); // Проверка обычных уведомлений
|
||||
void on_btnNotifyCheckMod_clicked(); // Проверка уведомлений для модераторов
|
||||
void on_btnNotifyCheckVip_clicked(); // Проверка уведомлений для VIP
|
||||
void on_btnNotifyCheckSub_clicked(); // Проверка уведомлений для подписчиков
|
||||
|
||||
void on_btnNotifyOpen_clicked(); // Открытие обычных уведомлений
|
||||
void on_btnNotifyOpenMod_clicked(); // Открытие уведомлений для модераторов
|
||||
void on_btnNotifyOpenVip_clicked(); // Открытие уведомлений для VIP
|
||||
void on_btnNotifyOpenSub_clicked(); // Открытие уведомлений для подписчиков
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С WEBSOCKET (TWITCH)
|
||||
// ========================================================================
|
||||
void onStatus(const QString &statusText, int statusCode);
|
||||
void onDisconnected(const QString &reason);
|
||||
void onJoined(const QString &nickname);
|
||||
void onMessage(const QString &message);
|
||||
void handleNewMessage(const QString &message);
|
||||
void handleError(const QString &errorMessage);
|
||||
void handleConnected();
|
||||
void handleDisconnected();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С КОМАНДАМИ И ОТВЕТАМИ
|
||||
// ========================================================================
|
||||
void execCommand(const QString &sender, const QString &message);
|
||||
QString ResponsParserStatic(const QString &inMess, const QString &adName, const QString &aCommandText);
|
||||
QString ResponsParserRandoms(const QString &inMess);
|
||||
QString ResponsParserSounds(const QString &inMess);
|
||||
QString ResponsParserText(const QString &inMess);
|
||||
QString ResponsParserAI(const QString &response, const QString &pall);
|
||||
QString responsParserAPI(const QString &inMess, const QString &adName);
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ИСКУССТВЕННЫМ ИНТЕЛЛЕКТОМ
|
||||
// ========================================================================
|
||||
void onAIResponseReceived(const QString &response);
|
||||
void onAIErrorOccurred(const QString &error);
|
||||
void onNeuralResponse(const QString &response);
|
||||
void onNeuralError(const QString &error);
|
||||
void sendRequestToAI(const QString &q);
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С TWITCH API
|
||||
// ========================================================================
|
||||
void onApiError(const QString &method, const QString &error);
|
||||
void onTokenExpired(const QString &error);
|
||||
void onRateLimit();
|
||||
void initTwitchAPI();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ИНТЕРФЕЙСОМ
|
||||
// ========================================================================
|
||||
// Радиокнопки выбора режима
|
||||
void on_RBCustom_pressed();
|
||||
void on_rbGC_clicked();
|
||||
void on_rbDS_clicked();
|
||||
void on_rbCG_clicked();
|
||||
|
||||
// Кнопки управления
|
||||
void on_pushButton_2_clicked();
|
||||
void on_pushButton_clicked();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ЛОГАМИ
|
||||
// ========================================================================
|
||||
void applyLogFilter();
|
||||
void addLogToTable(const LogEntry &entry, int row);
|
||||
bool shouldShowLogEntry(int code);
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ТАБЛИЦАМИ И ГРИДАМИ
|
||||
// ========================================================================
|
||||
void on_sgCommands_cellDoubleClicked(int row, int column);
|
||||
void onSoundGridDoubleClicked();
|
||||
void onFilesGridDoubleClicked();
|
||||
void onNeiroGridDoubleClicked();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С РАНДОМНЫМИ ОТВЕТАМИ
|
||||
// ========================================================================
|
||||
void on_lbRandomGroup_itemClicked(QListWidgetItem *item);
|
||||
void on_lbRandomGroup_doubleClicked(const QModelIndex &index);
|
||||
void on_lbRandomRespons_itemDoubleClicked(QListWidgetItem *item);
|
||||
|
||||
void on_btnRandAdd_clicked();
|
||||
void on_btnRandDel_clicked();
|
||||
void on_btnRandomAdd_clicked();
|
||||
void on_btnRandomDel_clicked();
|
||||
void on_btnRmGroup_clicked();
|
||||
|
||||
void on_sgRandomInt_cellClicked(int row, int column);
|
||||
void on_sgRandomInt_cellDoubleClicked(int row, int column);
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ПОЛЬЗОВАТЕЛЯМИ
|
||||
// ========================================================================
|
||||
void on_btnAddUserName_clicked();
|
||||
void on_btnGetDateFollow_clicked();
|
||||
void on_btnGetAgeAccaunt_clicked();
|
||||
void on_btnRandomUserName_clicked();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С КОМАНДАМИ ЧАТА
|
||||
// ========================================================================
|
||||
void on_btnAddCommand_clicked();
|
||||
void on_btnRmCommand_clicked();
|
||||
void on_btnEdtCommand_clicked();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С КАНАЛОМ И ДОП. ФУНКЦИЯМИ
|
||||
// ========================================================================
|
||||
void on_btnGetChannelStat_clicked();
|
||||
void on_btnGPT_clicked();
|
||||
void on_btnAIPic_clicked();
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ТЕМАМИ
|
||||
// ========================================================================
|
||||
void on_cbTheme_currentIndexChanged(int index);
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// СЛОТЫ ДЛЯ РАБОТЫ С ВЕБ СЕРВЕРАМИ
|
||||
// ========================================================================
|
||||
void on_btnWSCreateChat_clicked();
|
||||
void on_btnWSCreateNotify_clicked();
|
||||
|
||||
void onChatServerUpdated(HttpServerChat *server, const QString &name);
|
||||
void on_sgWebServers_cellDoubleClicked(int row, int column);
|
||||
|
||||
void onChatServerCreated(HttpServerChat *server, const QString &name);
|
||||
void onNotifyServerCreated(HttpServer *server, const QString &name);
|
||||
|
||||
// Методы для внешнего добавления уведомлений и сообщений
|
||||
void addNotification(const QString &nickname = "", double amount = 0, const QString &type = "donation");
|
||||
QString processTwitchMessage(const TwitchMessage &msg);
|
||||
QString replaceTwitchEmotes(const QString &emotesData, const QString &message);
|
||||
QString replaceCustomEmotes(const QString &message);
|
||||
|
||||
|
||||
void sendMessageToServer(HttpServerChat *server, const QString &nickname,
|
||||
const QString &message);
|
||||
|
||||
// Обновленный метод addChatMessage
|
||||
void addChatMessage(const QString &nickname,
|
||||
const QString &message);
|
||||
|
||||
void on_www_currentChanged(int index);
|
||||
|
||||
void on_edtBotName_selectionChanged();
|
||||
|
||||
void on_edtBotName_editingFinished();
|
||||
|
||||
void on_edtBotToken_textEdited(const QString &arg1);
|
||||
|
||||
void on_edtBotToken_editingFinished();
|
||||
|
||||
void on_edtBotTokenStreamer_editingFinished();
|
||||
|
||||
void on_edtBotClientID_editingFinished();
|
||||
|
||||
void on_edtChannel_editingFinished();
|
||||
|
||||
void on_edtDAClientID_editingFinished();
|
||||
|
||||
void on_edtDAClientSecret_editingFinished();
|
||||
|
||||
void on_edtDARedirectURL_editingFinished();
|
||||
|
||||
void on_edtDACode_editingFinished();
|
||||
|
||||
void on_cbDAAutoLogin_checkStateChanged(const Qt::CheckState &arg1);
|
||||
|
||||
void on_cbDAAutoLogin_stateChanged(int arg1);
|
||||
|
||||
void on_edtGPTPrefix_editingFinished();
|
||||
|
||||
void on_edtAIP1_editingFinished();
|
||||
|
||||
void on_edtAIP2_editingFinished();
|
||||
|
||||
void on_edtAIP3_editingFinished();
|
||||
|
||||
void on_edtKandiKey_editingFinished();
|
||||
|
||||
void on_edtKandiSecret_editingFinished();
|
||||
|
||||
void on_cbOllama_stateChanged(int arg1);
|
||||
|
||||
void on_tbNotifyVolume_valueChanged(int value);
|
||||
|
||||
void on_edtNotifyFileName_editingFinished();
|
||||
|
||||
void on_edtNotifyFileNameMod_editingFinished();
|
||||
|
||||
void on_edtNotifyFileNameVip_editingFinished();
|
||||
|
||||
void on_edtNotifyFileNameSub_editingFinished();
|
||||
|
||||
void on_chEnNotify_stateChanged(int arg1);
|
||||
|
||||
void on_chEnNotifyMod_stateChanged(int arg1);
|
||||
|
||||
void on_chEnNotifyVip_stateChanged(int arg1);
|
||||
|
||||
void on_chEnNotifySub_stateChanged(int arg1);
|
||||
|
||||
void on_tbNotifyVolume_sliderPressed();
|
||||
|
||||
void on_tbNotifyVolume_sliderReleased();
|
||||
|
||||
void on_tbNotifyVolumeMod_sliderReleased();
|
||||
|
||||
void on_tbNotifyVolumeVip_sliderReleased();
|
||||
|
||||
void on_tbNotifyVolumeSub_sliderReleased();
|
||||
|
||||
void on_cbNotifyFileAgain1_stateChanged(int arg1);
|
||||
|
||||
void on_cbNotifyFileAgain2_stateChanged(int arg1);
|
||||
|
||||
void on_cbNotifyFileAgain3_stateChanged(int arg1);
|
||||
|
||||
void on_btnThemesFolder_clicked();
|
||||
|
||||
public slots:
|
||||
// Установка статуса подключения к Twitch
|
||||
void setTwitchConnected(bool connected);
|
||||
|
||||
private:
|
||||
// ========================================================================
|
||||
// ПРИВАТНЫЕ ЧЛЕНЫ КЛАССА
|
||||
// ========================================================================
|
||||
Ui::uGeneral *ui; // Указатель на интерфейс
|
||||
// Объекты для авторизации
|
||||
TAuth *m_authBot; // Авторизация бота
|
||||
TAuth *m_authStreamer; // Авторизация стримера
|
||||
TAuth *m_authDA; // Авторизация DA
|
||||
|
||||
uLink *fLinkForm; // Форма ссылок
|
||||
TTTVAuth *TTVAuth; // Данные авторизации Twitch
|
||||
UserManager *m_userManager; // Менеджер пользователей
|
||||
QList<LogEntry> allLogs; // Все записи логов
|
||||
|
||||
WebSocketClient *m_twitchClient; // WebSocket клиент для Twitch
|
||||
UserWidget* m_userWidget; // Виджет пользователя
|
||||
NeuralNetworkManager *nnManager; // Менеджер нейронных сетей
|
||||
|
||||
QList<TimerInfo> m_timers; // Список таймеров
|
||||
int m_nextTimerId = 1; // Следующий ID таймера
|
||||
bool m_isTwitchConnected = false; // Статус подключения к Twitch
|
||||
|
||||
// Менеджеры веб-серверов
|
||||
QList<HttpServer*> m_notificationServers;
|
||||
QList<HttpServerChat*> m_chatServers;
|
||||
|
||||
// Для связи с окнами создания
|
||||
FCreateNotify *m_createNotifyDialog;
|
||||
FCreateChat *m_createChatDialog;
|
||||
QVector<ChatBadge> m_chatBadges;
|
||||
// Методы для обработки бейджей и форматирования ника
|
||||
QString formatNicknameWithBadges(const TwitchMessage &msg);
|
||||
QString getBadgesHTML(const TwitchMessage &msg);
|
||||
QString getBadgeUrl(const QString &setId, int versionId);
|
||||
QVector<QIcon> tabIcons;
|
||||
// Структура для замены эмодзи
|
||||
struct EmoteReplacement {
|
||||
int start;
|
||||
int length;
|
||||
QString html;
|
||||
};
|
||||
|
||||
// Добавление серверов в списки и таблицу
|
||||
void addNotificationServer(HttpServer *server, const QString &name);
|
||||
void addChatServer(HttpServerChat *server, const QString &name);
|
||||
void addServerToTable(const QString &name, const QString &type, quint16 port,
|
||||
const QString &url, QObject *serverObj);
|
||||
|
||||
// Вспомогательные методы
|
||||
int findNotificationServerRow(HttpServer *server);
|
||||
int findChatServerRow(HttpServerChat *server);
|
||||
QString generateServerId() const;
|
||||
|
||||
|
||||
// Текущие настройки для формы
|
||||
QVariantMap m_currentSettings;
|
||||
|
||||
// ========================================================================
|
||||
// ПРИВАТНЫЕ МЕТОДЫ
|
||||
// ========================================================================
|
||||
|
||||
// Метод для загрузки списка QSS файлов
|
||||
void loadQssFiles();
|
||||
// Метод для применения QSS темы
|
||||
void applyStyleSheet(const QString &filename);
|
||||
|
||||
QString getUserFilePath(const QString &type, const QString &fileName) const;
|
||||
QString getUserDataPath() const;
|
||||
QString getSystemPath() const;
|
||||
void setupButtonIcons();
|
||||
void loadStylesFromDirectory(const QDir &stylesDir, const QString &category);
|
||||
void copyDefaultFiles();
|
||||
void initializeAppDataStructure();
|
||||
|
||||
// Настройка OAuth для Twitch
|
||||
void setupTwitchOAuth();
|
||||
|
||||
// Инициализация конфигурации ИИ
|
||||
void initAIConfig();
|
||||
void updateAIConfig();
|
||||
QString processAITag(const QString &input, const QString &sender);
|
||||
|
||||
// Работа с таймерами
|
||||
void updateTimerTable();
|
||||
void startTimer(TimerInfo& timerInfo);
|
||||
void stopTimer(TimerInfo& timerInfo);
|
||||
void sendTimedMessage(const TimerInfo& timerInfo);
|
||||
|
||||
// Воспроизведение уведомлений
|
||||
void playNotify(const bool &isMod, const bool &isVip, const bool &isSub);
|
||||
|
||||
|
||||
// Инициализация базы данных
|
||||
void initializeDatabase();
|
||||
// Настройка пользовательского интерфейса
|
||||
void setupUI();
|
||||
// Настройка таблиц интерфейса
|
||||
void setupTables();
|
||||
// Инициализация менеджеров
|
||||
void initializeManagers();
|
||||
// Настройка компонентов Twitch
|
||||
void setupTwitchComponents();
|
||||
// Настройка виджета пользователей
|
||||
void setupUserWidget();
|
||||
// Настройка обработчиков авторизации
|
||||
void setupAuthHandlers();
|
||||
// Инициализация нейронной сети
|
||||
void initializeNeuralNetwork();
|
||||
// Загрузка эмодзи
|
||||
void loadEmoties();
|
||||
void loadChatBadges();
|
||||
|
||||
// Обработка эмодзи
|
||||
void processEmotes(QString &message);
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif // UGENERAL_H
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "ulink.h"
|
||||
#include "ui_ulink.h"
|
||||
|
||||
uLink::uLink(QWidget *parent) :
|
||||
QMainWindow(parent),
|
||||
ui(new Ui::uLink)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowTitle("Получение токена бота");
|
||||
}
|
||||
|
||||
uLink::~uLink()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void uLink::setLinkText(const QString &text)
|
||||
{
|
||||
ui->mLink->setText(text);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef ULINK_H
|
||||
#define ULINK_H
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
namespace Ui {
|
||||
class uLink;
|
||||
}
|
||||
|
||||
class uLink : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit uLink(QWidget *parent = nullptr);
|
||||
~uLink();
|
||||
void setLinkText(const QString &text);
|
||||
|
||||
private:
|
||||
Ui::uLink *ui;
|
||||
};
|
||||
|
||||
#endif // ULINK_H
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>uLink</class>
|
||||
<widget class="QWidget" name="uLink">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>381</width>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Откройте ссылку в браузере бота</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QTextEdit" name="mLink">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>30</y>
|
||||
<width>381</width>
|
||||
<height>261</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "user.h"
|
||||
#include "twitchmessage.h"
|
||||
#include <QStringList>
|
||||
|
||||
void User::updateFromTwitchMessage(const TwitchMessage &msg)
|
||||
{
|
||||
// Базовые данные
|
||||
if (!msg.displayName.isEmpty()) {
|
||||
displayName = msg.displayName;
|
||||
}
|
||||
|
||||
if (!msg.color.isEmpty()) {
|
||||
color = msg.color;
|
||||
}
|
||||
|
||||
if (msg.userId > 0) {
|
||||
id = QString::number(msg.userId);
|
||||
login = msg.displayName.toLower(); // Логин обычно в нижнем регистре
|
||||
}
|
||||
|
||||
// Статусы
|
||||
isModerator = msg.isMod;
|
||||
isVIP = msg.isVIP;
|
||||
isSubscriber = msg.isSubscriber;
|
||||
isTurbo = msg.isTurbo;
|
||||
returningChatter = msg.returningChatter;
|
||||
|
||||
if (msg.firstMsg) {
|
||||
firstMsg = true;
|
||||
}
|
||||
|
||||
// Бейджи
|
||||
badges.clear();
|
||||
for (const auto &badge : msg.badges) {
|
||||
badges.append(UserBadge(badge.name, badge.version));
|
||||
}
|
||||
|
||||
// Время последнего сообщения
|
||||
lastMessageTime = msg.timestamp();
|
||||
|
||||
// Увеличиваем счетчик сообщений
|
||||
messageCount++;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef USER_H
|
||||
#define USER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QVector>
|
||||
|
||||
struct UserBadge {
|
||||
QString name;
|
||||
int version;
|
||||
|
||||
UserBadge(const QString &name = "", int version = 0)
|
||||
: name(name), version(version) {}
|
||||
};
|
||||
|
||||
struct User {
|
||||
QString id;
|
||||
QString login;
|
||||
QString displayName;
|
||||
QString color;
|
||||
QDate followAt;
|
||||
QDate createdAt;
|
||||
QDateTime lastMessageTime;
|
||||
QVector<UserBadge> badges;
|
||||
|
||||
// Статусы
|
||||
bool isModerator;
|
||||
bool isVIP;
|
||||
bool isSubscriber;
|
||||
bool isTurbo;
|
||||
bool returningChatter;
|
||||
bool firstMsg;
|
||||
|
||||
// Статистика
|
||||
int messageCount;
|
||||
int warnCount;
|
||||
|
||||
User()
|
||||
: followAt(QDate(1900, 1, 1))
|
||||
, createdAt(QDate(1900, 1, 1))
|
||||
, lastMessageTime(QDateTime::fromSecsSinceEpoch(0))
|
||||
, isModerator(false)
|
||||
, isVIP(false)
|
||||
, isSubscriber(false)
|
||||
, isTurbo(false)
|
||||
, returningChatter(false)
|
||||
, firstMsg(false)
|
||||
, messageCount(0)
|
||||
, warnCount(0)
|
||||
{}
|
||||
|
||||
// Проверка наличия бейджа
|
||||
bool hasBadge(const QString &badgeName) const {
|
||||
for (const auto &badge : badges) {
|
||||
if (badge.name == badgeName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получение версии бейджа
|
||||
int badgeVersion(const QString &badgeName) const {
|
||||
for (const auto &badge : badges) {
|
||||
if (badge.name == badgeName) {
|
||||
return badge.version;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Обновление из TwitchMessage
|
||||
void updateFromTwitchMessage(const class TwitchMessage &msg);
|
||||
|
||||
// Строковое представление статусов
|
||||
QString getStatusString() const {
|
||||
QStringList statuses;
|
||||
if (isModerator) statuses << "Модератор";
|
||||
if (isVIP) statuses << "VIP";
|
||||
if (isSubscriber) statuses << "Подписчик";
|
||||
if (returningChatter) statuses << "Возвращающийся";
|
||||
if (firstMsg) statuses << "Первое сообщение";
|
||||
|
||||
return statuses.join(", ");
|
||||
}
|
||||
|
||||
// Полный список бейджей как строка
|
||||
QString getBadgesString() const {
|
||||
QStringList badgeStrings;
|
||||
for (const auto &badge : badges) {
|
||||
badgeStrings << QString("%1/%2").arg(badge.name).arg(badge.version);
|
||||
}
|
||||
return badgeStrings.join(",");
|
||||
}
|
||||
};
|
||||
|
||||
#endif // USER_H
|
||||
@@ -0,0 +1,316 @@
|
||||
#include "user_manager.h"
|
||||
#include "twitchmessage.h"
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
UserManager::UserManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_totalMessages(0)
|
||||
, m_maxUsers(10000)
|
||||
, m_cleanupInterval(60)
|
||||
, m_lastCleanup(QDateTime::currentDateTime())
|
||||
{
|
||||
}
|
||||
|
||||
UserManager::~UserManager()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
QString UserManager::checkUser(const QString &displayName, const TwitchMessage &msg)
|
||||
{
|
||||
QString normalizedDisplayName = displayName.trimmed();
|
||||
|
||||
// Проверяем, есть ли пользователь
|
||||
if (m_users.contains(normalizedDisplayName)) {
|
||||
User &user = m_users[normalizedDisplayName];
|
||||
|
||||
// Обновляем из сообщения, если передано
|
||||
if (!msg.displayName.isEmpty()) {
|
||||
updateUserFromMessage(normalizedDisplayName, msg);
|
||||
}
|
||||
|
||||
emit userMessageCountChanged(user.id, user.messageCount);
|
||||
emit userUpdated(user);
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
|
||||
// Пользователя нет - создаем нового
|
||||
User newUser;
|
||||
|
||||
if (!msg.displayName.isEmpty()) {
|
||||
// Заполняем из сообщения
|
||||
newUser.updateFromTwitchMessage(msg);
|
||||
} else {
|
||||
// Минимальные данные
|
||||
newUser.displayName = normalizedDisplayName;
|
||||
newUser.login = normalizedDisplayName.toLower();
|
||||
newUser.id = generateUserId();
|
||||
}
|
||||
|
||||
// Добавляем в базу
|
||||
addUser(newUser);
|
||||
|
||||
return newUser.id;
|
||||
}
|
||||
|
||||
User* UserManager::findUser(const QString &displayName)
|
||||
{
|
||||
if (m_users.contains(displayName)) {
|
||||
return &m_users[displayName];
|
||||
}
|
||||
|
||||
// Пробуем найти по login
|
||||
if (m_loginToDisplayName.contains(displayName.toLower())) {
|
||||
QString actualDisplayName = m_loginToDisplayName[displayName.toLower()];
|
||||
return &m_users[actualDisplayName];
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
User* UserManager::findUserById(const QString &userId)
|
||||
{
|
||||
if (m_userIdToDisplayName.contains(userId)) {
|
||||
QString displayName = m_userIdToDisplayName[userId];
|
||||
return &m_users[displayName];
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UserManager::updateUserFromMessage(const QString &displayName, const TwitchMessage &msg)
|
||||
{
|
||||
if (!m_users.contains(displayName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
User &user = m_users[displayName];
|
||||
|
||||
// ВАЖНО: передаем displayName как параметр, а не берем из msg
|
||||
// если displayName из параметра отличается от msg.displayName,
|
||||
// нам нужно обновить ключ в map
|
||||
|
||||
QString newDisplayName = msg.displayName;
|
||||
if (!newDisplayName.isEmpty() && newDisplayName != displayName) {
|
||||
// Display name изменился - нужно переместить пользователя
|
||||
User userCopy = user;
|
||||
userCopy.updateFromTwitchMessage(msg);
|
||||
|
||||
// Удаляем старую запись
|
||||
removeUser(displayName);
|
||||
|
||||
// Добавляем с новым displayName
|
||||
userCopy.displayName = newDisplayName;
|
||||
addUser(userCopy);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Display name не изменился - просто обновляем
|
||||
user.updateFromTwitchMessage(msg);
|
||||
|
||||
|
||||
|
||||
// Обновляем индексы
|
||||
updateIndexes(displayName, user);
|
||||
|
||||
emit userUpdated(user);
|
||||
}
|
||||
|
||||
void UserManager::updateUserStatus(const QString &userId, bool isMod, bool isVIP, bool isSubscriber)
|
||||
{
|
||||
User* user = findUserById(userId);
|
||||
if (user) {
|
||||
user->isModerator = isMod;
|
||||
user->isVIP = isVIP;
|
||||
user->isSubscriber = isSubscriber;
|
||||
|
||||
emit userUpdated(*user);
|
||||
}
|
||||
}
|
||||
|
||||
void UserManager::addUser(const User &user)
|
||||
{
|
||||
QString displayName = user.displayName;
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
qWarning() << "Cannot add user with empty display name";
|
||||
return;
|
||||
}
|
||||
|
||||
// Очистка старых пользователей, если нужно
|
||||
if (m_users.size() >= m_maxUsers) {
|
||||
cleanupOldUsers();
|
||||
}
|
||||
|
||||
m_users[displayName] = user;
|
||||
updateIndexes(displayName, user);
|
||||
|
||||
emit userAdded(user);
|
||||
}
|
||||
|
||||
void UserManager::removeUser(const QString &displayName)
|
||||
{
|
||||
if (m_users.contains(displayName)) {
|
||||
User user = m_users[displayName];
|
||||
|
||||
// Удаляем из индексов
|
||||
m_userIdToDisplayName.remove(user.id);
|
||||
m_loginToDisplayName.remove(user.login.toLower());
|
||||
|
||||
// Удаляем из основного списка
|
||||
m_users.remove(displayName);
|
||||
|
||||
emit userRemoved(user.id, displayName); // Передаем и id, и displayName
|
||||
}
|
||||
}
|
||||
|
||||
void UserManager::clear()
|
||||
{
|
||||
m_users.clear();
|
||||
m_userIdToDisplayName.clear();
|
||||
m_loginToDisplayName.clear();
|
||||
m_totalMessages = 0;
|
||||
}
|
||||
|
||||
QVector<User*> UserManager::getModerators() const
|
||||
{
|
||||
QVector<User*> moderators;
|
||||
|
||||
for (auto it = m_users.begin(); it != m_users.end(); ++it) {
|
||||
if (it.value().isModerator) {
|
||||
moderators.append(const_cast<User*>(&it.value()));
|
||||
}
|
||||
}
|
||||
|
||||
return moderators;
|
||||
}
|
||||
|
||||
QVector<User*> UserManager::getVIPs() const
|
||||
{
|
||||
QVector<User*> vips;
|
||||
|
||||
for (auto it = m_users.begin(); it != m_users.end(); ++it) {
|
||||
if (it.value().isVIP) {
|
||||
vips.append(const_cast<User*>(&it.value()));
|
||||
}
|
||||
}
|
||||
|
||||
return vips;
|
||||
}
|
||||
|
||||
QVector<User*> UserManager::getSubscribers() const
|
||||
{
|
||||
QVector<User*> subscribers;
|
||||
|
||||
for (auto it = m_users.begin(); it != m_users.end(); ++it) {
|
||||
if (it.value().isSubscriber) {
|
||||
subscribers.append(const_cast<User*>(&it.value()));
|
||||
}
|
||||
}
|
||||
|
||||
return subscribers;
|
||||
}
|
||||
|
||||
QVector<User*> UserManager::getActiveUsers(int minutes) const
|
||||
{
|
||||
QVector<User*> activeUsers;
|
||||
QDateTime cutoff = QDateTime::currentDateTime().addSecs(-minutes * 60);
|
||||
|
||||
for (auto it = m_users.begin(); it != m_users.end(); ++it) {
|
||||
if (it.value().lastMessageTime >= cutoff) {
|
||||
activeUsers.append(const_cast<User*>(&it.value()));
|
||||
}
|
||||
}
|
||||
|
||||
return activeUsers;
|
||||
}
|
||||
|
||||
void UserManager::updateIndexes(const QString &displayName, const User &user)
|
||||
{
|
||||
if (!user.id.isEmpty()) {
|
||||
m_userIdToDisplayName[user.id] = displayName;
|
||||
}
|
||||
|
||||
if (!user.login.isEmpty()) {
|
||||
m_loginToDisplayName[user.login.toLower()] = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
void UserManager::cleanupOldUsers()
|
||||
{
|
||||
QDateTime cutoff = QDateTime::currentDateTime().addDays(-7); // Удаляем неактивных неделю
|
||||
|
||||
QVector<QString> usersToRemove;
|
||||
|
||||
for (auto it = m_users.begin(); it != m_users.end(); ++it) {
|
||||
// Удаляем если:
|
||||
// 1. Не писал больше недели
|
||||
// 2. Не модератор/VIP/подписчик
|
||||
if (it.value().lastMessageTime < cutoff &&
|
||||
!it.value().isModerator &&
|
||||
!it.value().isVIP &&
|
||||
!it.value().isSubscriber &&
|
||||
it.value().messageCount < 5) { // И написано меньше 5 сообщений
|
||||
|
||||
usersToRemove.append(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &displayName : usersToRemove) {
|
||||
removeUser(displayName);
|
||||
}
|
||||
|
||||
m_lastCleanup = QDateTime::currentDateTime();
|
||||
}
|
||||
|
||||
int UserManager::getUserCount() const
|
||||
{
|
||||
return m_users.size();
|
||||
}
|
||||
|
||||
int UserManager::getMessageCount() const
|
||||
{
|
||||
return m_totalMessages;
|
||||
}
|
||||
|
||||
QVector<User*> UserManager::findUsersByLogin(const QString &login)
|
||||
{
|
||||
QVector<User*> result;
|
||||
QString lowerLogin = login.toLower();
|
||||
|
||||
for (auto it = m_users.begin(); it != m_users.end(); ++it) {
|
||||
if (it.value().login.toLower().contains(lowerLogin)) {
|
||||
result.append(&it.value());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
User* UserManager::getUserByIndex(int index)
|
||||
{
|
||||
if (index >= 0 && index < m_users.size()) {
|
||||
auto it = m_users.begin();
|
||||
std::advance(it, index);
|
||||
return &it.value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QMap<QString, User>& UserManager::getAllUsers() const
|
||||
{
|
||||
return m_users;
|
||||
}
|
||||
|
||||
QString UserManager::generateUserId() const
|
||||
{
|
||||
return QString("user_%1").arg(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef USER_MANAGER_H
|
||||
#define USER_MANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
#include <QDateTime>
|
||||
#include "user.h"
|
||||
#include "twitchmessage.h"
|
||||
|
||||
class UserManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UserManager(QObject *parent = nullptr);
|
||||
~UserManager();
|
||||
|
||||
// Основные методы
|
||||
QString checkUser(const QString &displayName, const TwitchMessage &msg = TwitchMessage());
|
||||
User* findUser(const QString &displayName);
|
||||
User* findUserById(const QString &userId);
|
||||
// Статистика
|
||||
int m_totalMessages;
|
||||
// Обновление пользователя
|
||||
void updateUserFromMessage(const QString &displayName, const TwitchMessage &msg);
|
||||
void updateUserStatus(const QString &userId, bool isMod, bool isVIP, bool isSubscriber);
|
||||
|
||||
// Управление списком
|
||||
void addUser(const User &user);
|
||||
void removeUser(const QString &displayName);
|
||||
void clear();
|
||||
|
||||
// Поиск
|
||||
QVector<User*> findUsersByLogin(const QString &login);
|
||||
QVector<User*> getModerators() const;
|
||||
QVector<User*> getVIPs() const;
|
||||
QVector<User*> getSubscribers() const;
|
||||
QVector<User*> getActiveUsers(int minutes = 10) const;
|
||||
|
||||
// Статистика
|
||||
int getUserCount() const;
|
||||
int getMessageCount() const;
|
||||
|
||||
// Сохранение/загрузка
|
||||
bool saveToFile(const QString &filename);
|
||||
bool loadFromFile(const QString &filename);
|
||||
|
||||
// Геттеры
|
||||
User* getUserByIndex(int index);
|
||||
const QMap<QString, User>& getAllUsers() const;
|
||||
|
||||
signals:
|
||||
void userAdded(const User &user);
|
||||
void userUpdated(const User &user);
|
||||
void userRemoved(const QString &userId, const QString &displayName);
|
||||
void userMessageCountChanged(const QString &userId, int count);
|
||||
|
||||
private:
|
||||
// Основное хранилище (DisplayName -> User)
|
||||
QMap<QString, User> m_users;
|
||||
|
||||
// Индексы для быстрого поиска
|
||||
QMap<QString, QString> m_userIdToDisplayName; // userId -> displayName
|
||||
QMap<QString, QString> m_loginToDisplayName; // login -> displayName
|
||||
|
||||
|
||||
|
||||
// Вспомогательные методы
|
||||
QString generateUserId() const;
|
||||
void updateIndexes(const QString &displayName, const User &user);
|
||||
void cleanupOldUsers();
|
||||
|
||||
// Конфигурация
|
||||
int m_maxUsers; // Максимальное количество пользователей в памяти
|
||||
int m_cleanupInterval; // Интервал очистки в минутах
|
||||
QDateTime m_lastCleanup;
|
||||
};
|
||||
|
||||
#endif // USER_MANAGER_H
|
||||
@@ -0,0 +1,708 @@
|
||||
#include "userwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
UserWidget::UserWidget(UserManager* userManager, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_userManager(userManager)
|
||||
, m_currentFilter(FilterAll)
|
||||
, m_contextMenu(nullptr)
|
||||
{
|
||||
setupUI();
|
||||
|
||||
m_tableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_tableWidget, &QTableWidget::customContextMenuRequested,
|
||||
this, &UserWidget::showContextMenu);
|
||||
|
||||
setupContextMenu(); // Добавляем инициализацию контекстного меню
|
||||
// Подключаем сигналы если менеджер уже есть
|
||||
if (m_userManager) {
|
||||
connectSignals();
|
||||
}
|
||||
|
||||
// Таймер для периодического обновления
|
||||
m_refreshTimer = new QTimer(this);
|
||||
connect(m_refreshTimer, &QTimer::timeout, this, &UserWidget::onRefreshTimer);
|
||||
m_refreshTimer->start(30000); // 30 секунд
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
UserWidget::~UserWidget()
|
||||
{
|
||||
if (m_refreshTimer) {
|
||||
m_refreshTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::setUserManager(UserManager* userManager)
|
||||
{
|
||||
// Отключаем старые соединения если были
|
||||
if (m_userManager) {
|
||||
disconnectSignals();
|
||||
}
|
||||
|
||||
m_userManager = userManager;
|
||||
|
||||
if (m_userManager) {
|
||||
connectSignals();
|
||||
}
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
void UserWidget::connectSignals()
|
||||
{
|
||||
connect(m_userManager, &UserManager::userAdded,
|
||||
this, &UserWidget::onUserAdded);
|
||||
connect(m_userManager, &UserManager::userUpdated,
|
||||
this, &UserWidget::onUserUpdated);
|
||||
connect(m_userManager, &UserManager::userRemoved,
|
||||
this, &UserWidget::onUserRemoved);
|
||||
}
|
||||
|
||||
void UserWidget::disconnectSignals()
|
||||
{
|
||||
disconnect(m_userManager, &UserManager::userAdded,
|
||||
this, &UserWidget::onUserAdded);
|
||||
disconnect(m_userManager, &UserManager::userUpdated,
|
||||
this, &UserWidget::onUserUpdated);
|
||||
disconnect(m_userManager, &UserManager::userRemoved,
|
||||
this, &UserWidget::onUserRemoved);
|
||||
}
|
||||
|
||||
void UserWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(5, 5, 5, 5);
|
||||
mainLayout->setSpacing(5);
|
||||
|
||||
// Панель инструментов
|
||||
QHBoxLayout* toolbarLayout = new QHBoxLayout();
|
||||
|
||||
// Фильтр
|
||||
QLabel* filterLabel = new QLabel("Фильтр:", this);
|
||||
m_filterCombo = new QComboBox(this);
|
||||
m_filterCombo->addItem("Все зрители", FilterAll);
|
||||
m_filterCombo->addItem("Модераторы", FilterModerators);
|
||||
m_filterCombo->addItem("VIP", FilterVIPs);
|
||||
m_filterCombo->addItem("Подписчики", FilterSubscribers);
|
||||
m_filterCombo->addItem("Активные (10 мин)", FilterActive);
|
||||
connect(m_filterCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &UserWidget::onFilterChanged);
|
||||
|
||||
// Поиск
|
||||
QLabel* searchLabel = new QLabel("Поиск:", this);
|
||||
m_searchEdit = new QLineEdit(this);
|
||||
m_searchEdit->setPlaceholderText("Введите имя зрителя...");
|
||||
m_searchEdit->setClearButtonEnabled(true);
|
||||
connect(m_searchEdit, &QLineEdit::textChanged,
|
||||
this, &UserWidget::onSearchTextChanged);
|
||||
|
||||
// Кнопка обновления
|
||||
QPushButton* refreshBtn = new QPushButton("Обновить", this);
|
||||
refreshBtn->setFixedWidth(100);
|
||||
connect(refreshBtn, &QPushButton::clicked,
|
||||
this, &UserWidget::onRefreshButtonClicked);
|
||||
|
||||
toolbarLayout->addWidget(filterLabel);
|
||||
toolbarLayout->addWidget(m_filterCombo);
|
||||
toolbarLayout->addWidget(searchLabel);
|
||||
toolbarLayout->addWidget(m_searchEdit);
|
||||
toolbarLayout->addStretch();
|
||||
toolbarLayout->addWidget(refreshBtn);
|
||||
|
||||
// Таблица
|
||||
m_tableWidget = new QTableWidget(this);
|
||||
m_tableWidget->setColumnCount(6);
|
||||
m_tableWidget->setHorizontalHeaderLabels(
|
||||
QStringList() << "Имя" << "Сообщений" << "Статус"
|
||||
<< "VIP" << "Подписчик" << "Последняя активность");
|
||||
|
||||
// Настройка таблицы
|
||||
m_tableWidget->setSortingEnabled(true);
|
||||
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableWidget->setAlternatingRowColors(true);
|
||||
m_tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tableWidget->horizontalHeader()->setStretchLastSection(true);
|
||||
m_tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
|
||||
m_tableWidget->verticalHeader()->setVisible(false);
|
||||
|
||||
// Настройка ширины колонок
|
||||
m_tableWidget->setColumnWidth(0, 250); // Имя
|
||||
m_tableWidget->setColumnWidth(1, 80); // Сообщений
|
||||
m_tableWidget->setColumnWidth(2, 100); // Статус
|
||||
m_tableWidget->setColumnWidth(3, 50); // VIP
|
||||
m_tableWidget->setColumnWidth(4, 80); // Подписчик
|
||||
// Последняя колонка растягивается
|
||||
|
||||
connect(m_tableWidget, &QTableWidget::cellDoubleClicked,
|
||||
this, &UserWidget::onUserDoubleClicked);
|
||||
|
||||
// Статистика внизу
|
||||
QHBoxLayout* statsLayout = new QHBoxLayout();
|
||||
m_statsLabel = new QLabel(this);
|
||||
statsLayout->addWidget(m_statsLabel);
|
||||
statsLayout->addStretch();
|
||||
|
||||
// Добавляем всё на форму
|
||||
mainLayout->addLayout(toolbarLayout);
|
||||
mainLayout->addWidget(m_tableWidget);
|
||||
mainLayout->addLayout(statsLayout);
|
||||
|
||||
setLayout(mainLayout);
|
||||
}
|
||||
|
||||
void UserWidget::refreshData()
|
||||
{
|
||||
if (!m_userManager) {
|
||||
m_tableWidget->setRowCount(0);
|
||||
m_statsLabel->setText("Менеджер пользователей не установлен");
|
||||
return;
|
||||
}
|
||||
|
||||
clearTable();
|
||||
populateTable();
|
||||
updateStatistics();
|
||||
}
|
||||
|
||||
void UserWidget::populateTable()
|
||||
{
|
||||
if (!m_userManager) return;
|
||||
|
||||
QVector<User*> users;
|
||||
|
||||
// Получаем пользователей в зависимости от фильтра
|
||||
switch (m_currentFilter) {
|
||||
case FilterAll:
|
||||
for (const User& user : m_userManager->getAllUsers()) {
|
||||
users.append(const_cast<User*>(&user));
|
||||
}
|
||||
break;
|
||||
case FilterModerators:
|
||||
users = m_userManager->getModerators();
|
||||
break;
|
||||
case FilterVIPs:
|
||||
users = m_userManager->getVIPs();
|
||||
break;
|
||||
case FilterSubscribers:
|
||||
users = m_userManager->getSubscribers();
|
||||
break;
|
||||
case FilterActive:
|
||||
users = m_userManager->getActiveUsers(10);
|
||||
break;
|
||||
}
|
||||
|
||||
// Применяем поисковый фильтр
|
||||
if (!m_searchText.isEmpty()) {
|
||||
QString searchLower = m_searchText.toLower();
|
||||
QVector<User*> filteredUsers;
|
||||
|
||||
for (User* user : users) {
|
||||
if (user->displayName.toLower().contains(searchLower) ||
|
||||
user->login.toLower().contains(searchLower)) {
|
||||
filteredUsers.append(user);
|
||||
}
|
||||
}
|
||||
users = filteredUsers;
|
||||
}
|
||||
|
||||
// Заполняем таблицу
|
||||
m_tableWidget->setRowCount(users.size());
|
||||
|
||||
for (int i = 0; i < users.size(); ++i) {
|
||||
User* user = users[i];
|
||||
|
||||
// Имя
|
||||
QTableWidgetItem* nameItem = new QTableWidgetItem(user->displayName);
|
||||
nameItem->setData(Qt::UserRole, user->id);
|
||||
m_tableWidget->setItem(i, 0, nameItem);
|
||||
|
||||
// Количество сообщений
|
||||
m_tableWidget->setItem(i, 1,
|
||||
new QTableWidgetItem(QString::number(user->messageCount)));
|
||||
|
||||
// Статус
|
||||
QString status;
|
||||
if (user->isModerator) status = "👑 Модератор";
|
||||
else if (user->isVIP) status = "⭐ VIP";
|
||||
else if (user->isSubscriber) status = "💎 Подписчик";
|
||||
else status = "👤 Зритель";
|
||||
m_tableWidget->setItem(i, 2, new QTableWidgetItem(status));
|
||||
|
||||
// VIP
|
||||
QTableWidgetItem* vipItem = new QTableWidgetItem(user->isVIP ? "Да" : "Нет");
|
||||
if (user->isVIP) {
|
||||
vipItem->setForeground(Qt::darkGreen);
|
||||
}
|
||||
m_tableWidget->setItem(i, 3, vipItem);
|
||||
|
||||
// Подписчик
|
||||
QTableWidgetItem* subItem = new QTableWidgetItem(user->isSubscriber ? "Да" : "Нет");
|
||||
if (user->isSubscriber) {
|
||||
subItem->setForeground(Qt::darkBlue);
|
||||
}
|
||||
m_tableWidget->setItem(i, 4, subItem);
|
||||
|
||||
// Последняя активность
|
||||
QString lastActive;
|
||||
if (user->lastMessageTime.isValid()) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
qint64 secs = user->lastMessageTime.secsTo(now);
|
||||
|
||||
if (secs < 60) {
|
||||
lastActive = "Только что";
|
||||
} else if (secs < 3600) {
|
||||
lastActive = QString("%1 мин назад").arg(secs / 60);
|
||||
} else if (secs < 86400) {
|
||||
lastActive = QString("%1 ч назад").arg(secs / 3600);
|
||||
} else {
|
||||
lastActive = user->lastMessageTime.toString("dd.MM.yyyy hh:mm");
|
||||
}
|
||||
} else {
|
||||
lastActive = "Никогда";
|
||||
}
|
||||
m_tableWidget->setItem(i, 5, new QTableWidgetItem(lastActive));
|
||||
}
|
||||
|
||||
// Автоподгон ширины колонок после заполнения
|
||||
m_tableWidget->resizeColumnsToContents();
|
||||
}
|
||||
|
||||
void UserWidget::updateStatistics()
|
||||
{
|
||||
if (!m_userManager) return;
|
||||
|
||||
QString stats = QString("Зрителей: %1 | Сообщений: %2 | Модераторов: %3 | VIP: %4 | Подписчиков: %5")
|
||||
.arg(m_userManager->getUserCount())
|
||||
.arg(m_userManager->getMessageCount())
|
||||
.arg(m_userManager->getModerators().size())
|
||||
.arg(m_userManager->getVIPs().size())
|
||||
.arg(m_userManager->getSubscribers().size());
|
||||
|
||||
m_statsLabel->setText(stats);
|
||||
}
|
||||
|
||||
void UserWidget::addUserToTable(const User &user)
|
||||
{
|
||||
int row = m_tableWidget->rowCount();
|
||||
m_tableWidget->insertRow(row);
|
||||
|
||||
// Имя
|
||||
QTableWidgetItem* nameItem = new QTableWidgetItem(user.displayName);
|
||||
nameItem->setData(Qt::UserRole, user.id);
|
||||
m_tableWidget->setItem(row, 0, nameItem);
|
||||
|
||||
// Количество сообщений
|
||||
m_tableWidget->setItem(row, 1,
|
||||
new QTableWidgetItem(QString::number(user.messageCount)));
|
||||
|
||||
// Статус
|
||||
QString status;
|
||||
if (user.isModerator) status = "👑 Модератор";
|
||||
else if (user.isVIP) status = "⭐ VIP";
|
||||
else if (user.isSubscriber) status = "💎 Подписчик";
|
||||
else status = "👤 Зритель";
|
||||
m_tableWidget->setItem(row, 2, new QTableWidgetItem(status));
|
||||
|
||||
// VIP
|
||||
QTableWidgetItem* vipItem = new QTableWidgetItem(user.isVIP ? "Да" : "Нет");
|
||||
if (user.isVIP) {
|
||||
vipItem->setForeground(Qt::darkGreen);
|
||||
}
|
||||
m_tableWidget->setItem(row, 3, vipItem);
|
||||
|
||||
// Подписчик
|
||||
QTableWidgetItem* subItem = new QTableWidgetItem(user.isSubscriber ? "Да" : "Нет");
|
||||
if (user.isSubscriber) {
|
||||
subItem->setForeground(Qt::darkBlue);
|
||||
}
|
||||
m_tableWidget->setItem(row, 4, subItem);
|
||||
|
||||
// Последняя активность
|
||||
QString lastActive = "Только что";
|
||||
m_tableWidget->setItem(row, 5, new QTableWidgetItem(lastActive));
|
||||
|
||||
updateStatistics();
|
||||
}
|
||||
|
||||
void UserWidget::updateUserInTable(const User &user)
|
||||
{
|
||||
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
|
||||
QTableWidgetItem* item = m_tableWidget->item(row, 0);
|
||||
if (item && item->data(Qt::UserRole).toString() == user.id) {
|
||||
// Обновляем данные
|
||||
item->setText(user.displayName);
|
||||
|
||||
m_tableWidget->item(row, 1)->setText(
|
||||
QString::number(user.messageCount));
|
||||
|
||||
QString status;
|
||||
if (user.isModerator) status = "👑 Модератор";
|
||||
else if (user.isVIP) status = "⭐ VIP";
|
||||
else if (user.isSubscriber) status = "💎 Подписчик";
|
||||
else status = "👤 Зритель";
|
||||
m_tableWidget->item(row, 2)->setText(status);
|
||||
|
||||
m_tableWidget->item(row, 3)->setText(user.isVIP ? "Да" : "Нет");
|
||||
if (user.isVIP) {
|
||||
m_tableWidget->item(row, 3)->setForeground(Qt::darkGreen);
|
||||
} else {
|
||||
m_tableWidget->item(row, 3)->setForeground(Qt::black);
|
||||
}
|
||||
|
||||
m_tableWidget->item(row, 4)->setText(user.isSubscriber ? "Да" : "Нет");
|
||||
if (user.isSubscriber) {
|
||||
m_tableWidget->item(row, 4)->setForeground(Qt::darkBlue);
|
||||
} else {
|
||||
m_tableWidget->item(row, 4)->setForeground(Qt::black);
|
||||
}
|
||||
|
||||
// Обновляем время активности
|
||||
QString lastActive;
|
||||
if (user.lastMessageTime.isValid()) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
qint64 secs = user.lastMessageTime.secsTo(now);
|
||||
|
||||
if (secs < 60) {
|
||||
lastActive = "Только что";
|
||||
} else if (secs < 3600) {
|
||||
lastActive = QString("%1 мин назад").arg(secs / 60);
|
||||
} else if (secs < 86400) {
|
||||
lastActive = QString("%1 ч назад").arg(secs / 3600);
|
||||
} else {
|
||||
lastActive = user.lastMessageTime.toString("dd.MM hh:mm");
|
||||
}
|
||||
}
|
||||
m_tableWidget->item(row, 5)->setText(lastActive);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateStatistics();
|
||||
}
|
||||
|
||||
void UserWidget::removeUserFromTable(const QString &displayName)
|
||||
{
|
||||
// Ищем пользователя по displayName
|
||||
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
|
||||
QTableWidgetItem* item = m_tableWidget->item(row, 0);
|
||||
if (item && item->text() == displayName) {
|
||||
m_tableWidget->removeRow(row);
|
||||
updateStatistics();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::clearTable()
|
||||
{
|
||||
m_tableWidget->setRowCount(0);
|
||||
}
|
||||
|
||||
void UserWidget::onRefreshTimer()
|
||||
{
|
||||
if (!m_userManager) return;
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
|
||||
QTableWidgetItem* idItem = m_tableWidget->item(row, 0);
|
||||
if (idItem) {
|
||||
QString userId = idItem->data(Qt::UserRole).toString();
|
||||
User* user = m_userManager->findUserById(userId);
|
||||
if (user && user->lastMessageTime.isValid()) {
|
||||
qint64 secs = user->lastMessageTime.secsTo(now);
|
||||
|
||||
QString lastActive;
|
||||
if (secs < 60) {
|
||||
lastActive = "Только что";
|
||||
} else if (secs < 3600) {
|
||||
lastActive = QString("%1 мин назад").arg(secs / 60);
|
||||
} else if (secs < 86400) {
|
||||
lastActive = QString("%1 ч назад").arg(secs / 3600);
|
||||
} else {
|
||||
lastActive = user->lastMessageTime.toString("dd.MM hh:mm");
|
||||
}
|
||||
|
||||
// Проверяем, существует ли ячейка
|
||||
if (m_tableWidget->item(row, 5)) {
|
||||
m_tableWidget->item(row, 5)->setText(lastActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UserWidget::userMatchesFilter(const User* user) const
|
||||
{
|
||||
if (!user) return false;
|
||||
|
||||
// Проверка фильтра
|
||||
switch (m_currentFilter) {
|
||||
case FilterAll:
|
||||
break;
|
||||
case FilterModerators:
|
||||
if (!user->isModerator) return false;
|
||||
break;
|
||||
case FilterVIPs:
|
||||
if (!user->isVIP) return false;
|
||||
break;
|
||||
case FilterSubscribers:
|
||||
if (!user->isSubscriber) return false;
|
||||
break;
|
||||
case FilterActive:
|
||||
// Активные за последние 10 минут
|
||||
if (user->lastMessageTime.secsTo(QDateTime::currentDateTime()) > 10*60)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Проверка поиска
|
||||
if (!m_searchText.isEmpty()) {
|
||||
QString searchLower = m_searchText.toLower();
|
||||
if (!user->displayName.toLower().contains(searchLower) &&
|
||||
!user->login.toLower().contains(searchLower)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UserWidget::onUserAdded(const User &user)
|
||||
{
|
||||
// Проверяем, проходит ли пользователь текущий фильтр и поиск
|
||||
if (userMatchesFilter(&user)) {
|
||||
addUserToTable(user);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onUserUpdated(const User &user)
|
||||
{
|
||||
// Ищем пользователя в таблице
|
||||
int row = findRowByUserId(user.id);
|
||||
|
||||
if (row >= 0) {
|
||||
// Пользователь уже в таблице
|
||||
if (userMatchesFilter(&user)) {
|
||||
// Обновляем существующую запись
|
||||
updateUserInTable(user);
|
||||
} else {
|
||||
// Удаляем, так как пользователь больше не проходит фильтр
|
||||
removeUserFromTableByUserId(user.id);
|
||||
}
|
||||
} else {
|
||||
// Пользователя нет в таблице
|
||||
if (userMatchesFilter(&user)) {
|
||||
// Добавляем, так как пользователь теперь проходит фильтр
|
||||
addUserToTable(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int UserWidget::findRowByUserId(const QString &userId) const
|
||||
{
|
||||
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
|
||||
QTableWidgetItem* item = m_tableWidget->item(row, 0);
|
||||
if (item && item->data(Qt::UserRole).toString() == userId) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void UserWidget::removeUserFromTableByUserId(const QString &userId)
|
||||
{
|
||||
int row = findRowByUserId(userId);
|
||||
if (row >= 0) {
|
||||
m_tableWidget->removeRow(row);
|
||||
updateStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onUserRemoved(const QString &userId, const QString &displayName)
|
||||
{
|
||||
Q_UNUSED(displayName);
|
||||
removeUserFromTableByUserId(userId);
|
||||
}
|
||||
|
||||
void UserWidget::onUserDoubleClicked(int row, int column)
|
||||
{
|
||||
Q_UNUSED(column);
|
||||
|
||||
if (!m_userManager) return;
|
||||
|
||||
QTableWidgetItem* item = m_tableWidget->item(row, 0);
|
||||
if (item) {
|
||||
QString userId = item->data(Qt::UserRole).toString();
|
||||
User* user = m_userManager->findUserById(userId);
|
||||
if (user) {
|
||||
qDebug() << "Детали зрителя:"
|
||||
<< "\nИмя:" << user->displayName
|
||||
<< "\nID:" << user->id
|
||||
<< "\nLogin:" << user->login
|
||||
<< "\nСообщений:" << user->messageCount
|
||||
<< "\nМодератор:" << user->isModerator
|
||||
<< "\nVIP:" << user->isVIP
|
||||
<< "\nПодписчик:" << user->isSubscriber
|
||||
<< "\nПоследнее сообщение:" << user->lastMessageTime.toString();
|
||||
|
||||
// Здесь можно открыть диалог с детальной информацией
|
||||
// или выполнить другие действия
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onFilterChanged(int index)
|
||||
{
|
||||
m_currentFilter = static_cast<FilterType>(m_filterCombo->itemData(index).toInt());
|
||||
refreshData();
|
||||
}
|
||||
|
||||
void UserWidget::onSearchTextChanged(const QString &text)
|
||||
{
|
||||
m_searchText = text;
|
||||
refreshData();
|
||||
}
|
||||
|
||||
void UserWidget::onRefreshButtonClicked()
|
||||
{
|
||||
refreshData();
|
||||
}
|
||||
|
||||
void UserWidget::setupContextMenu()
|
||||
{
|
||||
m_contextMenu = new QMenu(this);
|
||||
|
||||
m_banAction = m_contextMenu->addAction("Забанить");
|
||||
connect(m_banAction, &QAction::triggered, this, &UserWidget::onBanUser);
|
||||
|
||||
m_timeoutAction = m_contextMenu->addAction("Таймаут (5 мин)");
|
||||
connect(m_timeoutAction, &QAction::triggered, this, &UserWidget::onTimeoutUser);
|
||||
|
||||
m_unbanAction = m_contextMenu->addAction("Разбанить");
|
||||
connect(m_unbanAction, &QAction::triggered, this, &UserWidget::onUnbanUser);
|
||||
|
||||
m_contextMenu->addSeparator();
|
||||
|
||||
m_setModAction = m_contextMenu->addAction("Сделать модератором");
|
||||
connect(m_setModAction, &QAction::triggered, this, &UserWidget::onSetModerator);
|
||||
|
||||
m_removeModAction = m_contextMenu->addAction("Забрать модератора");
|
||||
connect(m_removeModAction, &QAction::triggered, this, &UserWidget::onRemoveModerator);
|
||||
|
||||
m_contextMenu->addSeparator();
|
||||
|
||||
m_setVIPAction = m_contextMenu->addAction("Сделать VIP");
|
||||
connect(m_setVIPAction, &QAction::triggered, this, &UserWidget::onSetVIP);
|
||||
|
||||
m_removeVIPAction = m_contextMenu->addAction("Забрать VIP");
|
||||
connect(m_removeVIPAction, &QAction::triggered, this, &UserWidget::onRemoveVIP);
|
||||
|
||||
m_contextMenu->addSeparator();
|
||||
|
||||
m_infoAction = m_contextMenu->addAction("Информация");
|
||||
connect(m_infoAction, &QAction::triggered, this, &UserWidget::onShowUserInfo);
|
||||
}
|
||||
|
||||
// Добавляем обработчик контекстного меню:
|
||||
void UserWidget::showContextMenu(const QPoint &pos)
|
||||
{
|
||||
QTableWidgetItem* item = m_tableWidget->itemAt(pos);
|
||||
|
||||
if (!item) {
|
||||
return; // Клик был не по ячейке (например, по заголовку или пустой области)
|
||||
}
|
||||
|
||||
int row = item->row();
|
||||
QTableWidgetItem* nameItem = m_tableWidget->item(row, 0);
|
||||
|
||||
if (nameItem) {
|
||||
m_selectedUserId = nameItem->data(Qt::UserRole).toString();
|
||||
m_selectedUserName = nameItem->text();
|
||||
|
||||
// Получаем данные пользователя
|
||||
User* user = m_userManager->findUserById(m_selectedUserId);
|
||||
if (user) {
|
||||
// Обновляем состояние пунктов меню
|
||||
m_setModAction->setEnabled(!user->isModerator);
|
||||
m_removeModAction->setEnabled(user->isModerator);
|
||||
m_setVIPAction->setEnabled(!user->isVIP);
|
||||
m_removeVIPAction->setEnabled(user->isVIP);
|
||||
|
||||
// Показываем меню в позиции клика
|
||||
m_contextMenu->exec(m_tableWidget->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем вспомогательный метод:
|
||||
User* UserWidget::getSelectedUser()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty()) {
|
||||
return m_userManager->findUserById(m_selectedUserId);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Добавляем слоты для обработки действий меню:
|
||||
void UserWidget::onBanUser()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit banUserRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onTimeoutUser()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit timeoutUserRequested(m_selectedUserId, m_selectedUserName, 5);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onUnbanUser()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit unbanUserRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onSetModerator()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit setModeratorRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onRemoveModerator()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit removeModeratorRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onSetVIP()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit setVIPRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onRemoveVIP()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit removeVIPRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserWidget::onShowUserInfo()
|
||||
{
|
||||
if (!m_selectedUserId.isEmpty() && !m_selectedUserName.isEmpty()) {
|
||||
emit userInfoRequested(m_selectedUserId, m_selectedUserName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#ifndef USERWIDGET_H
|
||||
#define USERWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTimer>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QContextMenuEvent>
|
||||
#include "user_manager.h"
|
||||
|
||||
class UserWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UserWidget(UserManager* userManager, QWidget *parent = nullptr);
|
||||
~UserWidget();
|
||||
|
||||
void setUserManager(UserManager* userManager);
|
||||
void refreshData();
|
||||
void updateStatistics();
|
||||
|
||||
signals:
|
||||
void banUserRequested(const QString &userId, const QString &userName);
|
||||
void timeoutUserRequested(const QString &userId, const QString &userName, int minutes);
|
||||
void unbanUserRequested(const QString &userId, const QString &userName);
|
||||
void setModeratorRequested(const QString &userId, const QString &userName);
|
||||
void removeModeratorRequested(const QString &userId, const QString &userName);
|
||||
void setVIPRequested(const QString &userId, const QString &userName);
|
||||
void removeVIPRequested(const QString &userId, const QString &userName);
|
||||
void userInfoRequested(const QString &userId, const QString &userName);
|
||||
|
||||
private slots:
|
||||
void onRefreshTimer();
|
||||
void onUserAdded(const User &user);
|
||||
void onUserUpdated(const User &user);
|
||||
void onUserRemoved(const QString &userId, const QString &displayName);
|
||||
void onUserDoubleClicked(int row, int column);
|
||||
void onFilterChanged(int index);
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onRefreshButtonClicked();
|
||||
|
||||
// Слоты для контекстного меню
|
||||
void onBanUser();
|
||||
void onTimeoutUser();
|
||||
void onUnbanUser();
|
||||
void onSetModerator();
|
||||
void onRemoveModerator();
|
||||
void onSetVIP();
|
||||
void onRemoveVIP();
|
||||
void onShowUserInfo();
|
||||
|
||||
void showContextMenu(const QPoint &pos);
|
||||
protected:
|
||||
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void populateTable();
|
||||
void addUserToTable(const User &user);
|
||||
void updateUserInTable(const User &user);
|
||||
void removeUserFromTable(const QString &displayName);
|
||||
void clearTable();
|
||||
void disconnectSignals();
|
||||
void connectSignals();
|
||||
void setupContextMenu();
|
||||
bool userMatchesFilter(const User* user) const;
|
||||
int findRowByUserId(const QString &userId) const;
|
||||
void removeUserFromTableByUserId(const QString &userId);
|
||||
User* getSelectedUser();
|
||||
|
||||
UserManager* m_userManager;
|
||||
|
||||
// UI элементы
|
||||
QTableWidget* m_tableWidget;
|
||||
QComboBox* m_filterCombo;
|
||||
QLineEdit* m_searchEdit;
|
||||
QLabel* m_statsLabel;
|
||||
|
||||
QTimer* m_refreshTimer;
|
||||
|
||||
// Контекстное меню
|
||||
QMenu* m_contextMenu;
|
||||
QAction* m_banAction;
|
||||
QAction* m_timeoutAction;
|
||||
QAction* m_unbanAction;
|
||||
QAction* m_setModAction;
|
||||
QAction* m_removeModAction;
|
||||
QAction* m_setVIPAction;
|
||||
QAction* m_removeVIPAction;
|
||||
QAction* m_infoAction;
|
||||
|
||||
enum FilterType {
|
||||
FilterAll,
|
||||
FilterModerators,
|
||||
FilterVIPs,
|
||||
FilterSubscribers,
|
||||
FilterActive
|
||||
};
|
||||
|
||||
FilterType m_currentFilter;
|
||||
QString m_searchText;
|
||||
|
||||
// Выбранный пользователь для контекстного меню
|
||||
QString m_selectedUserId;
|
||||
QString m_selectedUserName;
|
||||
};
|
||||
|
||||
#endif // USERWIDGET_H
|
||||
@@ -0,0 +1,739 @@
|
||||
#include "webserverchat.h"
|
||||
#include "qcolor.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QFileInfo>
|
||||
#include <QDateTime>
|
||||
|
||||
HttpServerChat::HttpServerChat(const QStringList &fontList, int port, const QString &backgroundColor, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_server(nullptr)
|
||||
, m_fontList(fontList)
|
||||
, m_backgroundColor(backgroundColor)
|
||||
, m_deleteByTime(true) // По умолчанию удаление по времени
|
||||
, m_maxMsgCount(100) // Значение по умолчанию
|
||||
{
|
||||
m_server = new QTcpServer(this);
|
||||
m_server->listen(QHostAddress::Any, port);
|
||||
|
||||
connect(m_server, &QTcpServer::newConnection, this, &HttpServerChat::onNewConnection);
|
||||
m_currentStyle.freez = m_isFreez;
|
||||
|
||||
}
|
||||
|
||||
HttpServerChat::~HttpServerChat()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool HttpServerChat::start()
|
||||
{
|
||||
if (m_server->isListening()) {
|
||||
emit serverStarted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_server->listen(QHostAddress::Any, m_server->serverPort())) {
|
||||
|
||||
emit serverStarted(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
emit serverStarted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpServerChat::stop()
|
||||
{
|
||||
if (m_server) {
|
||||
m_server->close();
|
||||
}
|
||||
|
||||
for (auto client : m_clients) {
|
||||
if (client->state() == QAbstractSocket::ConnectedState) {
|
||||
client->close();
|
||||
}
|
||||
client->deleteLater();
|
||||
}
|
||||
m_clients.clear();
|
||||
}
|
||||
|
||||
quint16 HttpServerChat::port() const
|
||||
{
|
||||
return m_server ? m_server->serverPort() : 0;
|
||||
}
|
||||
|
||||
void HttpServerChat::addMessage(const StyleChat &style)
|
||||
{
|
||||
m_currentStyle = style;
|
||||
|
||||
ChatMessage msg;
|
||||
msg.nickname = style.nick;
|
||||
msg.content = style.context;
|
||||
msg.timestamp = QDateTime::currentSecsSinceEpoch();
|
||||
msg.timeMsg = style.timeMsg;
|
||||
msg.freez = style.freez;
|
||||
msg.transparency = style.transparency;
|
||||
|
||||
// Копируем стилевые параметры
|
||||
msg.blockColor = style.blockColor;
|
||||
msg.borderColor = style.borderColor;
|
||||
msg.fontColor = style.fontColor;
|
||||
msg.fontFamily = style.fontFamily;
|
||||
msg.fontSize = style.fontSize;
|
||||
msg.borderSize = style.borderSize;
|
||||
msg.padding = style.padding;
|
||||
|
||||
if (!m_deleteByTime) {
|
||||
// Режим удаления по количеству
|
||||
// Проверяем, не превышен ли лимит
|
||||
if (m_messages.size() >= m_maxMsgCount) {
|
||||
// Ищем незамороженное сообщение для удаления
|
||||
bool removed = false;
|
||||
for (int i = 0; i < m_messages.size(); ++i) {
|
||||
if (!m_messages[i].freez) {
|
||||
m_messages.removeAt(i);
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если все сообщения заморожены и лимит превышен,
|
||||
// не добавляем новое сообщение
|
||||
if (!removed) {
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_messages.append(msg);
|
||||
|
||||
|
||||
|
||||
// Обновляем всех подключенных клиентов
|
||||
for (auto client : m_clients) {
|
||||
if (client->state() == QAbstractSocket::ConnectedState) {
|
||||
// Отправляем JavaScript для обновления страницы
|
||||
QString js = "<script>window.location.reload();</script>";
|
||||
sendResponse(client, "text/html", js);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServerChat::activeServer(bool enabled)
|
||||
{
|
||||
if (enabled) {
|
||||
if (!m_server->isListening()) {
|
||||
m_server->listen(QHostAddress::Any, m_server->serverPort());
|
||||
}
|
||||
} else {
|
||||
m_server->close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void HttpServerChat::changeBackground(const QString &color)
|
||||
{
|
||||
m_backgroundColor = color;
|
||||
}
|
||||
|
||||
|
||||
void HttpServerChat::onNewConnection()
|
||||
{
|
||||
while (m_server->hasPendingConnections()) {
|
||||
QTcpSocket *socket = m_server->nextPendingConnection();
|
||||
m_clients.append(socket);
|
||||
|
||||
connect(socket, &QTcpSocket::readyRead, this, &HttpServerChat::readClient);
|
||||
connect(socket, &QTcpSocket::disconnected, this, &HttpServerChat::discardClient);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServerChat::readClient()
|
||||
{
|
||||
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
|
||||
if (!socket) return;
|
||||
|
||||
QByteArray requestData = socket->readAll();
|
||||
QString request = QString::fromUtf8(requestData);
|
||||
|
||||
if (!request.isEmpty()) {
|
||||
processRequest(socket, request);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServerChat::processRequest(QTcpSocket *socket, const QString &request)
|
||||
{
|
||||
QStringList lines = request.split("\r\n");
|
||||
if (lines.isEmpty()) return;
|
||||
|
||||
QString firstLine = lines[0];
|
||||
QStringList parts = firstLine.split(" ");
|
||||
if (parts.size() < 2) return;
|
||||
|
||||
QString method = parts[0];
|
||||
QString path = parts[1];
|
||||
|
||||
|
||||
|
||||
|
||||
cleanupOldMessages();
|
||||
|
||||
if (method == "GET") {
|
||||
if (path == "/" || path == "/index.html") {
|
||||
|
||||
sendHtmlPage(socket);
|
||||
} else if (path == "/messages") {
|
||||
|
||||
sendJSONMessages(socket);
|
||||
} else if (path.startsWith("/fonts/")) {
|
||||
QString filePath = path.mid(1); // Убираем первый слэш
|
||||
|
||||
serveStaticFile(socket, filePath);
|
||||
} else {
|
||||
// Игнорируем favicon и другие запросы
|
||||
if (!path.contains("favicon.ico") && !path.contains(".well-known")) {
|
||||
|
||||
}
|
||||
sendResponse(socket, "text/html", "", 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServerChat::sendHtmlPage(QTcpSocket *socket)
|
||||
{
|
||||
QString html = generateHTML();
|
||||
sendResponse(socket, "text/html", html);
|
||||
}
|
||||
|
||||
void HttpServerChat::sendJSONMessages(QTcpSocket *socket)
|
||||
{
|
||||
QString json = generateJSON();
|
||||
sendResponse(socket, "application/json; charset=utf-8", json);
|
||||
}
|
||||
|
||||
void HttpServerChat::serveStaticFile(QTcpSocket *socket, const QString &filePath)
|
||||
{
|
||||
QString fullPath = QCoreApplication::applicationDirPath() + "/" + filePath;
|
||||
QFile file(fullPath);
|
||||
|
||||
|
||||
|
||||
if (file.exists() && file.open(QIODevice::ReadOnly)) {
|
||||
QString mimeType = getMimeType(filePath);
|
||||
QByteArray content = file.readAll();
|
||||
file.close();
|
||||
|
||||
|
||||
|
||||
QString response = QString(
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: %1\r\n"
|
||||
"Content-Length: %2\r\n"
|
||||
"Cache-Control: no-cache\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n")
|
||||
.arg(mimeType)
|
||||
.arg(content.size());
|
||||
|
||||
socket->write(response.toUtf8());
|
||||
socket->write(content);
|
||||
socket->flush();
|
||||
} else {
|
||||
|
||||
sendResponse(socket, "text/html",
|
||||
"<html><body><h1>404 Not Found</h1><p>" + filePath + "</p></body></html>",
|
||||
404);
|
||||
}
|
||||
}
|
||||
|
||||
QString HttpServerChat::generateHTML()
|
||||
{
|
||||
// Генерация CSS для шрифтов
|
||||
QString fontFaces;
|
||||
for (const QString &fontFile : m_fontList) {
|
||||
QString fontName = fontFile;
|
||||
fontName.replace(".ttf", "").replace(".otf", "").replace(".ttc", "");
|
||||
fontFaces += QString("@font-face { font-family: '%1'; src: url(fonts/%2); }\n")
|
||||
.arg(fontName).arg(fontFile);
|
||||
}
|
||||
|
||||
QString deleteByTimeJS = m_deleteByTime ? "true" : "false";
|
||||
|
||||
QString html =
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
"<meta charset='UTF-8'>\n"
|
||||
"<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\">\n"
|
||||
"<meta http-equiv=\"Pragma\" content=\"no-cache\">\n"
|
||||
"<meta http-equiv=\"Expires\" content=\"0\">\n"
|
||||
"<title>Chat Messages</title>\n"
|
||||
"<style>\n"
|
||||
"body { background: %1; margin: 0; padding: 0; overflow: hidden; }\n"
|
||||
"#messages { \n"
|
||||
" position: fixed; \n"
|
||||
" bottom: 20px; \n"
|
||||
" right: 20px; \n"
|
||||
" width: 400px; \n"
|
||||
" display: flex; \n"
|
||||
" flex-direction: column; \n"
|
||||
" align-items: flex-end; \n"
|
||||
"}\n"
|
||||
".message { \n"
|
||||
" margin: 3px 0; \n"
|
||||
" border-radius: 5px; \n"
|
||||
" transition: opacity 0.5s linear; \n"
|
||||
" display: flex; \n"
|
||||
" align-items: center; \n"
|
||||
" opacity: 1;\n"
|
||||
" max-width: 100%;\n"
|
||||
" word-wrap: break-word;\n"
|
||||
"}\n"
|
||||
".message-icon { \n"
|
||||
" width: 1.5em; \n"
|
||||
" height: 1.5em; \n"
|
||||
" margin-right: 0.5em; \n"
|
||||
"}\n"
|
||||
"%2\n" // font faces
|
||||
"</style>\n"
|
||||
"<script>\n"
|
||||
"let existingMessages = new Map();\n"
|
||||
"let fetching = false;\n"
|
||||
"const deleteByTime = %3;\n"
|
||||
"\n"
|
||||
"function hexToRgb(hex) {\n"
|
||||
" console.log('hexToRgb input:', hex);\n"
|
||||
" \n"
|
||||
" // Создаем временный canvas элемент для парсинга цвета\n"
|
||||
" const ctx = document.createElement('canvas').getContext('2d');\n"
|
||||
" ctx.fillStyle = hex;\n"
|
||||
" const color = ctx.fillStyle;\n"
|
||||
" \n"
|
||||
" // Если цвет не распознан, используем значение по умолчанию\n"
|
||||
" if (!color || color === 'rgba(0, 0, 0, 0)') {\n"
|
||||
" console.log('Color not recognized, using default');\n"
|
||||
" return '74, 175, 80'; // #4CAF50\n"
|
||||
" }\n"
|
||||
" \n"
|
||||
" // Парсим цвет в формате rgb(r, g, b)\n"
|
||||
" const match = color.match(/rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)/);\n"
|
||||
" \n"
|
||||
" if (match) {\n"
|
||||
" console.log('Parsed RGB:', match[1], match[2], match[3]);\n"
|
||||
" return `${match[1]}, ${match[2]}, ${match[3]}`;\n"
|
||||
" }\n"
|
||||
" \n"
|
||||
" console.log('Failed to parse, using default');\n"
|
||||
" return '74, 175, 80'; // #4CAF50\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"function fetchMessages() {\n"
|
||||
" if (fetching) return;\n"
|
||||
" fetching = true;\n"
|
||||
" \n"
|
||||
" fetch('/messages')\n"
|
||||
" .then(response => response.json())\n"
|
||||
" .then(data => {\n"
|
||||
" const container = document.getElementById('messages');\n"
|
||||
" \n"
|
||||
" // 1. СОЗДАЕМ СЕТ ИЗ ВСЕХ ИМЕЮЩИХСЯ ID\n"
|
||||
" const newIds = new Set();\n"
|
||||
" data.forEach(msg => {\n"
|
||||
" const msgId = 'msg-' + msg.timestamp;\n"
|
||||
" newIds.add(msgId);\n"
|
||||
" });\n"
|
||||
" \n"
|
||||
" // 2. УДАЛЯЕМ ТОЛЬКО ТЕ СООБЩЕНИЯ, КОТОРЫХ НЕТ В НОВЫХ ДАННЫХ\n"
|
||||
" // И КОТОРЫЕ НЕ ЗАМОРОЖЕНЫ\n"
|
||||
" existingMessages.forEach((div, msgId) => {\n"
|
||||
" if (!newIds.has(msgId)) {\n"
|
||||
" // Проверяем, заморожено ли сообщение\n"
|
||||
" const isFreez = div.getAttribute('data-freez') === 'true';\n"
|
||||
" if (!isFreez) {\n"
|
||||
" div.style.opacity = '0';\n"
|
||||
" setTimeout(() => {\n"
|
||||
" if (div.parentNode) {\n"
|
||||
" div.parentNode.removeChild(div);\n"
|
||||
" }\n"
|
||||
" existingMessages.delete(msgId);\n"
|
||||
" }, 500);\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" });\n"
|
||||
" \n"
|
||||
" // 3. ДОБАВЛЯЕМ НОВЫЕ СООБЩЕНИЯ\n"
|
||||
" data.forEach(msg => {\n"
|
||||
" const msgId = 'msg-' + msg.timestamp;\n"
|
||||
" \n"
|
||||
" if (!existingMessages.has(msgId)) {\n"
|
||||
" const div = document.createElement('div');\n"
|
||||
" div.className = 'message';\n"
|
||||
" div.id = msgId;\n"
|
||||
" div.setAttribute('data-freez', msg.freez ? 'true' : 'false');\n"
|
||||
" \n"
|
||||
" // ПРИМЕНЯЕМ СТИЛИ\n"
|
||||
" function hexToRgb(hex) {\n"
|
||||
" console.log('hexToRgb input:', hex);\n"
|
||||
" if (!hex || !hex.startsWith('#')) {\n"
|
||||
" console.log('Not a hex color, using default');\n"
|
||||
" return '74, 175, 80'; // #4CAF50\n"
|
||||
" }\n"
|
||||
" // Убираем # и проверяем длину\n"
|
||||
" let cleanHex = hex.substring(1);\n"
|
||||
" // Убираем альфа-канал если есть\n"
|
||||
" if (cleanHex.length === 8) {\n"
|
||||
" cleanHex = cleanHex.substring(0, 6);\n"
|
||||
" }\n"
|
||||
" // Расширяем короткую форму (#rgb → #rrggbb)\n"
|
||||
" if (cleanHex.length === 3) {\n"
|
||||
" cleanHex = cleanHex[0] + cleanHex[0] + \n"
|
||||
" cleanHex[1] + cleanHex[1] + \n"
|
||||
" cleanHex[2] + cleanHex[2];\n"
|
||||
" }\n"
|
||||
" // Проверяем, что строка состоит из шестнадцатеричных символов\n"
|
||||
" if (cleanHex.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {\n"
|
||||
" console.log('Invalid hex format, using default');\n"
|
||||
" return '74, 175, 80'; // #4CAF50\n"
|
||||
" }\n"
|
||||
" // Конвертируем HEX в RGB\n"
|
||||
" const r = parseInt(cleanHex.substring(0, 2), 16);\n"
|
||||
" const g = parseInt(cleanHex.substring(2, 4), 16);\n"
|
||||
" const b = parseInt(cleanHex.substring(4, 6), 16);\n"
|
||||
" console.log('Parsed RGB:', r, g, b);\n"
|
||||
" return `${r}, ${g}, ${b}`;\n"
|
||||
" }\n"
|
||||
" \n"
|
||||
" // ФОРМИРУЕМ СОДЕРЖИМОЕ\n"
|
||||
" let content = '<span><b>' + msg.nickname + ':</b> ' + msg.content + '</span>';\n"
|
||||
" div.innerHTML = content;\n"
|
||||
" div.style.opacity = '1';\n"
|
||||
" \n"
|
||||
" container.appendChild(div);\n"
|
||||
" existingMessages.set(msgId, div);\n"
|
||||
" \n"
|
||||
" // 4. УСТАНАВЛИВАЕМ ТАЙМЕР УДАЛЕНИЯ ТОЛЬКО ЕСЛИ:\n"
|
||||
" // - включено удаление по времени (deleteByTime)\n"
|
||||
" // - сообщение НЕ заморожено (freez = false)\n"
|
||||
" if (deleteByTime && (!msg.freez || msg.freez === false)) {\n"
|
||||
" setTimeout(() => {\n"
|
||||
" if (existingMessages.has(msgId)) {\n"
|
||||
" const messageDiv = existingMessages.get(msgId);\n"
|
||||
" messageDiv.style.opacity = '0';\n"
|
||||
" setTimeout(() => {\n"
|
||||
" if (messageDiv.parentNode) {\n"
|
||||
" messageDiv.parentNode.removeChild(messageDiv);\n"
|
||||
" }\n"
|
||||
" existingMessages.delete(msgId);\n"
|
||||
" }, 500);\n"
|
||||
" }\n"
|
||||
" }, (parseInt(msg.timeMsg) || 10) * 1000);\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" });\n"
|
||||
" \n"
|
||||
" // 5. ОБРАБОТКА РЕЖИМА УДАЛЕНИЯ ПО КОЛИЧЕСТВУ\n"
|
||||
" if (!deleteByTime) {\n"
|
||||
" const maxMessages = 100;\n"
|
||||
" if (existingMessages.size > maxMessages) {\n"
|
||||
" const messagesArray = Array.from(existingMessages.entries());\n"
|
||||
" messagesArray.sort((a, b) => {\n"
|
||||
" const idA = parseInt(a[0].replace('msg-', ''));\n"
|
||||
" const idB = parseInt(b[0].replace('msg-', ''));\n"
|
||||
" return idA - idB;\n"
|
||||
" });\n"
|
||||
" \n"
|
||||
" let removedCount = 0;\n"
|
||||
" for (const [msgId, div] of messagesArray) {\n"
|
||||
" if (existingMessages.size - removedCount <= maxMessages) break;\n"
|
||||
" \n"
|
||||
" const isFreez = div.getAttribute('data-freez') === 'true';\n"
|
||||
" if (!isFreez) {\n"
|
||||
" div.style.opacity = '0';\n"
|
||||
" setTimeout(() => {\n"
|
||||
" if (div.parentNode) {\n"
|
||||
" div.parentNode.removeChild(div);\n"
|
||||
" }\n"
|
||||
" existingMessages.delete(msgId);\n"
|
||||
" }, 500);\n"
|
||||
" removedCount++;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" })\n"
|
||||
" .catch(error => console.error('Ошибка при загрузке сообщений:', error))\n"
|
||||
" .finally(() => { fetching = false; });\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"// Обновляем сообщения каждые 500 мс\n"
|
||||
"setInterval(fetchMessages, 500);\n"
|
||||
"\n"
|
||||
"// Первоначальная загрузка\n"
|
||||
"fetchMessages();\n"
|
||||
"</script>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"<div id='messages'></div>\n"
|
||||
"</body>\n"
|
||||
"</html>";
|
||||
|
||||
return html.arg(m_backgroundColor).arg(fontFaces).arg(deleteByTimeJS);
|
||||
}
|
||||
|
||||
QString HttpServerChat::generateJSON()
|
||||
{
|
||||
QJsonArray jsonArray;
|
||||
|
||||
for (const ChatMessage &msg : m_messages) {
|
||||
QJsonObject obj;
|
||||
obj["nickname"] = msg.nickname;
|
||||
obj["content"] = msg.content;
|
||||
obj["timestamp"] = msg.timestamp;
|
||||
obj["timeMsg"] = msg.timeMsg;
|
||||
obj["freez"] = msg.freez;
|
||||
obj["transparency"] = msg.transparency;
|
||||
|
||||
// Конвертируем цветовые названия в HEX формат
|
||||
QString blockColor = msg.blockColor.isEmpty() ? m_currentStyle.blockColor : msg.blockColor;
|
||||
QString borderColor = msg.borderColor.isEmpty() ? m_currentStyle.borderColor : msg.borderColor;
|
||||
QString fontColor = msg.fontColor.isEmpty() ? m_currentStyle.fontColor : msg.fontColor;
|
||||
// Конвертируем названия цветов в HEX формат
|
||||
obj["color"] = colorNameToHex(blockColor);
|
||||
obj["borderColor"] = colorNameToHex(borderColor);
|
||||
obj["fontColor"] = colorNameToHex(fontColor);
|
||||
|
||||
obj["family"] = msg.fontFamily.isEmpty() ? m_currentStyle.fontFamily : msg.fontFamily;
|
||||
obj["fontSize"] = msg.fontSize > 0 ? msg.fontSize : m_currentStyle.fontSize;
|
||||
obj["borderSize"] = msg.borderSize > 0 ? msg.borderSize : m_currentStyle.borderSize;
|
||||
obj["padding"] = msg.padding > 0 ? msg.padding : m_currentStyle.padding;
|
||||
|
||||
jsonArray.append(obj);
|
||||
}
|
||||
|
||||
QJsonDocument doc(jsonArray);
|
||||
return QString::fromUtf8(doc.toJson());
|
||||
}
|
||||
|
||||
QString HttpServerChat::colorNameToHex(const QString &colorName) const
|
||||
{
|
||||
if (colorName.isEmpty()) {
|
||||
return "#000000";
|
||||
}
|
||||
|
||||
// Если строка уже начинается с #, это HEX формат
|
||||
if (colorName.startsWith('#')) {
|
||||
return colorName;
|
||||
}
|
||||
|
||||
// Проверяем, является ли это названием цвета
|
||||
QColor color;
|
||||
if (color.isValidColor(colorName)) {
|
||||
color.setNamedColor(colorName);
|
||||
return color.name();
|
||||
}
|
||||
|
||||
// Пробуем создать QColor из строки
|
||||
color = QColor(colorName);
|
||||
if (color.isValid()) {
|
||||
return color.name();
|
||||
}
|
||||
|
||||
// Если ничего не получилось, возвращаем черный по умолчанию
|
||||
return "#000000";
|
||||
}
|
||||
|
||||
void HttpServerChat::sendResponse(QTcpSocket *socket, const QString &contentType, const QString &content, int statusCode)
|
||||
{
|
||||
QString statusText;
|
||||
switch(statusCode) {
|
||||
case 200: statusText = "OK"; break;
|
||||
case 404: statusText = "Not Found"; break;
|
||||
default: statusText = "OK"; break;
|
||||
}
|
||||
|
||||
QByteArray responseData = QString(
|
||||
"HTTP/1.1 %1 %2\r\n"
|
||||
"Content-Type: %3\r\n"
|
||||
"Content-Length: %4\r\n"
|
||||
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"Connection: keep-alive\r\n"
|
||||
"\r\n"
|
||||
"%5")
|
||||
.arg(statusCode)
|
||||
.arg(statusText)
|
||||
.arg(contentType)
|
||||
.arg(content.toUtf8().size())
|
||||
.arg(content)
|
||||
.toUtf8();
|
||||
|
||||
socket->write(responseData);
|
||||
socket->flush();
|
||||
}
|
||||
|
||||
QString HttpServerChat::getMimeType(const QString &filePath)
|
||||
{
|
||||
if (filePath.endsWith(".html")) return "text/html";
|
||||
if (filePath.endsWith(".css")) return "text/css";
|
||||
if (filePath.endsWith(".js")) return "application/javascript";
|
||||
if (filePath.endsWith(".png")) return "image/png";
|
||||
if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (filePath.endsWith(".gif")) return "image/gif";
|
||||
if (filePath.endsWith(".svg")) return "image/svg+xml";
|
||||
if (filePath.endsWith(".ico")) return "image/x-icon";
|
||||
if (filePath.endsWith(".ttf")) return "font/ttf";
|
||||
if (filePath.endsWith(".otf")) return "font/otf";
|
||||
if (filePath.endsWith(".woff")) return "font/woff";
|
||||
if (filePath.endsWith(".woff2")) return "font/woff2";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
void HttpServerChat::discardClient()
|
||||
{
|
||||
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
|
||||
if (socket) {
|
||||
m_clients.removeOne(socket);
|
||||
socket->deleteLater();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Реализация методов получения настроек
|
||||
QString HttpServerChat::getFontFamily() const {
|
||||
return m_currentStyle.fontFamily;
|
||||
}
|
||||
|
||||
int HttpServerChat::getFontSize() const {
|
||||
return m_currentStyle.fontSize;
|
||||
}
|
||||
|
||||
QString HttpServerChat::getFontColor() const {
|
||||
return m_currentStyle.fontColor;
|
||||
}
|
||||
|
||||
QString HttpServerChat::getBlockColor() const {
|
||||
return m_currentStyle.blockColor;
|
||||
}
|
||||
|
||||
QString HttpServerChat::getBorderColor() const {
|
||||
return m_currentStyle.borderColor;
|
||||
}
|
||||
|
||||
int HttpServerChat::getBorderSize() const {
|
||||
return m_currentStyle.borderSize;
|
||||
}
|
||||
|
||||
int HttpServerChat::getPadding() const {
|
||||
return m_currentStyle.padding;
|
||||
}
|
||||
|
||||
int HttpServerChat::getMessageTimeout() const {
|
||||
return m_currentStyle.timeMsg;
|
||||
}
|
||||
|
||||
QString HttpServerChat::getBackgroundColor() const {
|
||||
return m_backgroundColor;
|
||||
}
|
||||
|
||||
int HttpServerChat::getTransparency() const {
|
||||
return m_currentStyle.transparency;
|
||||
}
|
||||
|
||||
|
||||
void HttpServerChat::setFontFamily(const QString &fontFamily) {
|
||||
m_currentStyle.fontFamily = fontFamily;
|
||||
}
|
||||
|
||||
void HttpServerChat::setFontSize(int fontSize) {
|
||||
m_currentStyle.fontSize = fontSize;
|
||||
}
|
||||
|
||||
void HttpServerChat::setFontColor(const QString &fontColor) {
|
||||
m_currentStyle.fontColor = fontColor;
|
||||
}
|
||||
|
||||
void HttpServerChat::setBlockColor(const QString &blockColor) {
|
||||
m_currentStyle.blockColor = blockColor;
|
||||
}
|
||||
|
||||
void HttpServerChat::setBorderColor(const QString &borderColor) {
|
||||
m_currentStyle.borderColor = borderColor;
|
||||
}
|
||||
|
||||
void HttpServerChat::setBorderSize(int borderSize) {
|
||||
m_currentStyle.borderSize = borderSize;
|
||||
}
|
||||
|
||||
void HttpServerChat::setPadding(int padding) {
|
||||
m_currentStyle.padding = padding;
|
||||
}
|
||||
|
||||
void HttpServerChat::setMessageTimeout(int timeout) {
|
||||
m_currentStyle.timeMsg = timeout;
|
||||
}
|
||||
|
||||
void HttpServerChat::setTransparency(int transparency) {
|
||||
m_currentStyle.transparency = qBound(0, transparency, 255);
|
||||
}
|
||||
|
||||
void HttpServerChat::setDeleteMode(bool deleteByTime, int maxMsgCount)
|
||||
{
|
||||
m_deleteByTime = deleteByTime;
|
||||
m_maxMsgCount = maxMsgCount;
|
||||
|
||||
if (!deleteByTime) {
|
||||
// Режим удаления по количеству
|
||||
// Удаляем самые старые сообщения, НО сохраняем замороженные
|
||||
while (m_messages.size() > m_maxMsgCount) {
|
||||
// Ищем незамороженное сообщение для удаления
|
||||
bool removed = false;
|
||||
for (int i = 0; i < m_messages.size(); ++i) {
|
||||
if (!m_messages[i].freez) {
|
||||
m_messages.removeAt(i);
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если все сообщения заморожены, выходим из цикла
|
||||
if (!removed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Режим удаления по времени
|
||||
// Замороженные сообщения сохраняются в любом случае
|
||||
cleanupOldMessages();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServerChat::cleanupOldMessages()
|
||||
{
|
||||
if (!m_deleteByTime) {
|
||||
return; // Если удаление по времени отключено, выходим
|
||||
}
|
||||
|
||||
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
// Проходим сообщения в обратном порядке для безопасного удаления
|
||||
for (int i = m_messages.size() - 1; i >= 0; --i) {
|
||||
const ChatMessage &msg = m_messages[i];
|
||||
|
||||
// Пропускаем замороженные сообщения
|
||||
if (msg.freez) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qint64 messageAge = currentTime - msg.timestamp;
|
||||
if (messageAge >= msg.timeMsg) {
|
||||
m_messages.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
#ifndef WEBSERVERCHAT_H
|
||||
#define WEBSERVERCHAT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QMap>
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
#include <QList>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QDateTime>
|
||||
|
||||
// Структура для стиля чата
|
||||
struct StyleChat {
|
||||
QString nick;
|
||||
QString context;
|
||||
QString blockColor;
|
||||
QString bColor; // Цвет фона страницы
|
||||
int fontSize;
|
||||
QString fontColor;
|
||||
QString borderColor;
|
||||
int borderSize;
|
||||
int padding;
|
||||
QString fontFamily;
|
||||
int timeMsg; // Время отображения в секундах
|
||||
bool freez;
|
||||
int transparency;
|
||||
|
||||
StyleChat() :
|
||||
fontSize(14),
|
||||
borderSize(2),
|
||||
padding(5),
|
||||
timeMsg(10),
|
||||
freez(false),
|
||||
transparency(255)
|
||||
{}
|
||||
};
|
||||
|
||||
// Структура для сообщения чата
|
||||
struct ChatMessage {
|
||||
QString nickname;
|
||||
QString content;
|
||||
qint64 timestamp;
|
||||
int timeMsg;
|
||||
bool freez;
|
||||
int transparency; // Исправлено с transporant на transparency
|
||||
|
||||
// Поля для стиля
|
||||
QString blockColor;
|
||||
QString borderColor;
|
||||
QString fontColor;
|
||||
QString fontFamily;
|
||||
int fontSize;
|
||||
int borderSize;
|
||||
int padding;
|
||||
|
||||
ChatMessage() :
|
||||
timestamp(0),
|
||||
timeMsg(10),
|
||||
freez(false),
|
||||
transparency(255),
|
||||
fontSize(14),
|
||||
borderSize(2),
|
||||
padding(5)
|
||||
{}
|
||||
};
|
||||
|
||||
class HttpServerChat : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HttpServerChat(const QStringList &fontList, int port, const QString &backgroundColor, QObject *parent = nullptr);
|
||||
~HttpServerChat();
|
||||
|
||||
bool start();
|
||||
void stop();
|
||||
|
||||
|
||||
void addMessage(const StyleChat &style);
|
||||
void activeServer(bool enabled);
|
||||
void setDeleteMode(bool deleteByTime, int maxMsgCount);
|
||||
void changeBackground(const QString &color);
|
||||
bool isFreez() const { return m_isFreez; }
|
||||
void setFreez(bool freez) { m_isFreez = freez; m_currentStyle.freez = freez; }
|
||||
int getMaxMsgCount() const { return m_maxMsgCount; }
|
||||
void setMaxMsgCount(int count) { m_maxMsgCount = count; }
|
||||
|
||||
|
||||
// block color
|
||||
QString getBlockColor() const;
|
||||
QString getBorderColor() const;
|
||||
QString getBackgroundColor() const;
|
||||
int getBorderSize() const;
|
||||
int getPadding() const;
|
||||
int getTransparency() const;
|
||||
// block font
|
||||
QString getFontFamily() const;
|
||||
int getFontSize() const;
|
||||
QString getFontColor() const;
|
||||
// block settings
|
||||
int getMessageTimeout() const;
|
||||
quint16 port() const;
|
||||
|
||||
|
||||
void setFontFamily(const QString &fontFamily);
|
||||
void setFontSize(int fontSize);
|
||||
void setFontColor(const QString &fontColor);
|
||||
void setBlockColor(const QString &blockColor);
|
||||
void setBorderColor(const QString &borderColor);
|
||||
void setBorderSize(int borderSize);
|
||||
void setPadding(int padding);
|
||||
void setMessageTimeout(int timeout);
|
||||
|
||||
void setTransparency(int transparency);
|
||||
|
||||
|
||||
signals:
|
||||
void serverStarted(bool success);
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
void readClient();
|
||||
void discardClient();
|
||||
|
||||
private:
|
||||
QTcpServer *m_server;
|
||||
QList<QTcpSocket*> m_clients;
|
||||
QList<ChatMessage> m_messages;
|
||||
|
||||
// Настройки
|
||||
QStringList m_fontList;
|
||||
StyleChat m_currentStyle;
|
||||
QString m_backgroundColor;
|
||||
bool m_deleteByTime; // true - удаление по времени, false - по количеству
|
||||
int m_maxMsgCount; // Максимальное количество сообщений
|
||||
QString colorNameToHex(const QString &colorName) const;
|
||||
void processRequest(QTcpSocket *socket, const QString &request);
|
||||
void sendResponse(QTcpSocket *socket, const QString &contentType, const QString &content, int statusCode = 200);
|
||||
void sendHtmlPage(QTcpSocket *socket);
|
||||
void sendJSONMessages(QTcpSocket *socket);
|
||||
void serveStaticFile(QTcpSocket *socket, const QString &filePath);
|
||||
QString getMimeType(const QString &filePath);
|
||||
QString generateHTML();
|
||||
QString generateJSON();
|
||||
void cleanupOldMessages();
|
||||
bool m_isFreez;
|
||||
};
|
||||
|
||||
#endif // WEBSERVERCHAT_H
|
||||
@@ -0,0 +1,507 @@
|
||||
#include "webservernotify.h"
|
||||
#include <QDateTime>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QFileInfo>
|
||||
|
||||
HttpServer::HttpServer(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_server(nullptr)
|
||||
, m_pageBackgroundColor("transparent")
|
||||
{
|
||||
}
|
||||
|
||||
HttpServer::~HttpServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool HttpServer::start(quint16 port)
|
||||
{
|
||||
if (m_server) {
|
||||
stop();
|
||||
}
|
||||
|
||||
m_server = new QTcpServer(this);
|
||||
|
||||
if (!m_server->listen(QHostAddress::Any, port)) {
|
||||
|
||||
delete m_server;
|
||||
m_server = nullptr;
|
||||
emit serverStarted(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
connect(m_server, &QTcpServer::newConnection, this, &HttpServer::onNewConnection);
|
||||
|
||||
emit serverStarted(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpServer::stop()
|
||||
{
|
||||
if (m_server) {
|
||||
m_server->close();
|
||||
m_server->deleteLater();
|
||||
m_server = nullptr;
|
||||
}
|
||||
|
||||
// Закрываем всех клиентов
|
||||
for (auto client : m_clients) {
|
||||
if (client->state() == QAbstractSocket::ConnectedState) {
|
||||
client->close();
|
||||
}
|
||||
client->deleteLater();
|
||||
}
|
||||
m_clients.clear();
|
||||
}
|
||||
|
||||
quint16 HttpServer::port() const
|
||||
{
|
||||
return m_server ? m_server->serverPort() : 0;
|
||||
}
|
||||
|
||||
void HttpServer::addNotification(const Notification ¬ification)
|
||||
{
|
||||
cleanOldMessages();
|
||||
|
||||
// Добавляем временную метку
|
||||
Notification notif = notification;
|
||||
notif.timestamp = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
// Обновляем цвет фона страницы из уведомления
|
||||
if (!notification.pageBackgroundColor.isEmpty()) {
|
||||
m_pageBackgroundColor = notification.pageBackgroundColor;
|
||||
}
|
||||
|
||||
m_notifications.append(notif);
|
||||
|
||||
|
||||
|
||||
// Обновляем всех подключенных клиентов
|
||||
for (auto client : m_clients) {
|
||||
if (client->state() == QAbstractSocket::ConnectedState) {
|
||||
// Отправляем JavaScript для обновления страницы
|
||||
QString js = "<script>window.location.reload();</script>";
|
||||
sendResponse(client, "text/html", js);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::clearNotifications()
|
||||
{
|
||||
m_notifications.clear();
|
||||
}
|
||||
|
||||
void HttpServer::cleanOldMessages()
|
||||
{
|
||||
qint64 currentTime = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
for (int i = m_notifications.size() - 1; i >= 0; --i) {
|
||||
if (currentTime - m_notifications[i].timestamp >= m_notifications[i].duration) {
|
||||
m_notifications.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::onNewConnection()
|
||||
{
|
||||
while (m_server->hasPendingConnections()) {
|
||||
QTcpSocket *socket = m_server->nextPendingConnection();
|
||||
m_clients.append(socket);
|
||||
|
||||
// Устанавливаем keep-alive
|
||||
connect(socket, &QTcpSocket::readyRead, this, &HttpServer::readClient);
|
||||
connect(socket, &QTcpSocket::disconnected, this, &HttpServer::discardClient);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::readClient()
|
||||
{
|
||||
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
|
||||
if (!socket) return;
|
||||
|
||||
QByteArray requestData = socket->readAll();
|
||||
QString request = QString::fromUtf8(requestData);
|
||||
|
||||
if (!request.isEmpty()) {
|
||||
processRequest(socket, request);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::processRequest(QTcpSocket *socket, const QString &request)
|
||||
{
|
||||
QStringList lines = request.split("\r\n");
|
||||
if (lines.isEmpty()) return;
|
||||
|
||||
QString firstLine = lines[0];
|
||||
QStringList parts = firstLine.split(" ");
|
||||
if (parts.size() < 2) return;
|
||||
|
||||
QString method = parts[0];
|
||||
QString path = parts[1];
|
||||
|
||||
|
||||
|
||||
if (method == "GET") {
|
||||
if (path == "/" || path == "/index.html") {
|
||||
sendHtmlPage(socket);
|
||||
} else if (path == "/messages") {
|
||||
sendJSONMessages(socket);
|
||||
} else if (path.startsWith("/sounds/")) {
|
||||
QString filePath = path.mid(1); // Убираем первый слэш
|
||||
serveStaticFile(socket, filePath);
|
||||
} else if (path.startsWith("/imgs/")) {
|
||||
QString filePath = path.mid(1);
|
||||
serveStaticFile(socket, filePath);
|
||||
} else if (path.startsWith("/fonts/")) {
|
||||
QString filePath = path.mid(1);
|
||||
serveStaticFile(socket, filePath);
|
||||
} else if (path == "/clear") {
|
||||
clearNotifications();
|
||||
sendResponse(socket, "text/html", "<html><body>Notifications cleared</body></html>");
|
||||
} else {
|
||||
// Игнорируем favicon и другие запросы
|
||||
if (!path.contains("favicon.ico") && !path.contains(".well-known")) {
|
||||
}
|
||||
sendResponse(socket, "text/html", "", 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::sendHtmlPage(QTcpSocket *socket)
|
||||
{
|
||||
QString html = generateHTML();
|
||||
sendResponse(socket, "text/html", html);
|
||||
}
|
||||
|
||||
void HttpServer::sendJSONMessages(QTcpSocket *socket)
|
||||
{
|
||||
QString json = generateJSON();
|
||||
|
||||
|
||||
// Убедимся, что это валидный JSON
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument::fromJson(json.toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Ошибка парсинга JSON:" << parseError.errorString();
|
||||
json = "[]"; // Отправляем пустой массив при ошибке
|
||||
}
|
||||
|
||||
sendResponse(socket, "application/json; charset=utf-8", json);
|
||||
}
|
||||
|
||||
void HttpServer::serveStaticFile(QTcpSocket *socket, const QString &filePath)
|
||||
{
|
||||
QString fullPath = QCoreApplication::applicationDirPath() + "/" + filePath;
|
||||
QFile file(fullPath);
|
||||
|
||||
|
||||
|
||||
if (file.exists() && file.open(QIODevice::ReadOnly)) {
|
||||
QString mimeType = getMimeType(filePath);
|
||||
QByteArray content = file.readAll();
|
||||
file.close();
|
||||
|
||||
|
||||
QString response = QString(
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: %1\r\n"
|
||||
"Content-Length: %2\r\n"
|
||||
"Cache-Control: no-cache\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n")
|
||||
.arg(mimeType)
|
||||
.arg(content.size());
|
||||
|
||||
socket->write(response.toUtf8());
|
||||
socket->write(content);
|
||||
socket->flush();
|
||||
} else {
|
||||
sendResponse(socket, "text/html",
|
||||
"<html><body><h1>404 Not Found</h1><p>" + filePath + "</p></body></html>",
|
||||
404);
|
||||
}
|
||||
}
|
||||
|
||||
QString HttpServer::generateHTML()
|
||||
{
|
||||
QString html =
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
"<meta charset='UTF-8'>\n"
|
||||
"<title>Web Notifications</title>\n"
|
||||
"<style>\n"
|
||||
"body { background: %1; margin: 0; padding: 0; }\n" // Используем цвет фона страницы
|
||||
"#messages { position: fixed; top: 20px; right: 20px; width: 400px; }\n"
|
||||
".notification { \n"
|
||||
" margin: 10px 0; \n"
|
||||
" border-radius: 10px; \n"
|
||||
" padding: 15px; \n"
|
||||
" max-width: 100%; \n"
|
||||
" box-shadow: 0 4px 15px rgba(0,0,0,0.2);\n"
|
||||
" animation: fadeIn 0.3s ease-in;\n"
|
||||
" opacity: 1;\n"
|
||||
" transition: opacity 1s ease-out;\n"
|
||||
"}\n"
|
||||
".notification.fade-out {\n"
|
||||
" opacity: 0 !important;\n"
|
||||
"}\n"
|
||||
".nick { margin: 0 0 5px 0; padding: 0; }\n"
|
||||
".text { margin: 0; padding: 0; }\n"
|
||||
"@keyframes fadeIn {\n"
|
||||
" from { opacity: 0; transform: translateY(-20px); }\n"
|
||||
" to { opacity: 1; transform: translateY(0); }\n"
|
||||
"}\n"
|
||||
"</style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"<div id='messages'></div>\n"
|
||||
"<script>\n"
|
||||
"let notifications = new Map(); // Храним активные уведомления по timestamp\n"
|
||||
"let lastSoundTime = 0;\n"
|
||||
"\n"
|
||||
"function loadMessages() {\n"
|
||||
" fetch('/messages')\n"
|
||||
" .then(response => response.json())\n"
|
||||
" .then(data => {\n"
|
||||
" updateNotifications(data);\n"
|
||||
" })\n"
|
||||
" .catch(error => console.error('Error:', error));\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"function updateNotifications(messages) {\n"
|
||||
" const container = document.getElementById('messages');\n"
|
||||
" \n"
|
||||
" // Проходим по всем полученным сообщениям\n"
|
||||
" messages.forEach(msg => {\n"
|
||||
" const msgId = 'msg-' + msg.timestamp;\n"
|
||||
" \n"
|
||||
" // Если уведомление уже есть, пропускаем\n"
|
||||
" if (notifications.has(msgId)) {\n"
|
||||
" return;\n"
|
||||
" }\n"
|
||||
" \n"
|
||||
" // Создаем новое уведомление\n"
|
||||
" const div = document.createElement('div');\n"
|
||||
" div.className = 'notification';\n"
|
||||
" div.id = msgId;\n"
|
||||
" \n"
|
||||
" // Применяем стили из настроек\n"
|
||||
" div.style.backgroundColor = msg.color || '#4CAF50';\n"
|
||||
" div.style.border = (msg.borderSize || 2) + 'px solid ' + (msg.borderColor || '#2E7D32');\n"
|
||||
" div.style.padding = '15px';\n"
|
||||
" \n"
|
||||
" // Формируем содержимое\n"
|
||||
" let content = '';\n"
|
||||
" \n"
|
||||
" // Изображение\n"
|
||||
" if (msg.url && msg.url !== '') {\n"
|
||||
" content += '<img src=\"' + msg.url + '\" style=\"max-width: 100%; height: auto; border-radius: 5px; margin-bottom: 10px;\">';\n"
|
||||
" }\n"
|
||||
" \n"
|
||||
" // Заголовок\n"
|
||||
" content += '<div class=\"nick\" style=\"'\n"
|
||||
" + 'color: ' + (msg.titleColor || '#FFFFFF') + ';'\n"
|
||||
" + 'font-family: \"' + (msg.titleFamily || 'Arial') + '\", sans-serif;'\n"
|
||||
" + 'font-size: ' + (msg.titleSize || 20) + 'px;'\n"
|
||||
" + 'font-weight: bold;'\n"
|
||||
" + 'text-shadow: 1px 1px 2px rgba(0,0,0,0.5);'\n"
|
||||
" + '\">'\n"
|
||||
" + msg.nickname\n"
|
||||
" + '</div>';\n"
|
||||
" \n"
|
||||
" // Текст\n"
|
||||
" content += '<div class=\"text\" style=\"'\n"
|
||||
" + 'color: ' + (msg.contentColor || '#F5F5F5') + ';'\n"
|
||||
" + 'font-family: \"' + (msg.contentFamily || 'Arial') + '\", sans-serif;'\n"
|
||||
" + 'font-size: ' + (msg.contentSize || 16) + 'px;'\n"
|
||||
" + '\">'\n"
|
||||
" + msg.content\n"
|
||||
" + '</div>';\n"
|
||||
" \n"
|
||||
" div.innerHTML = content;\n"
|
||||
" container.appendChild(div);\n"
|
||||
" \n"
|
||||
" // Добавляем в карту\n"
|
||||
" notifications.set(msgId, {\n"
|
||||
" element: div,\n"
|
||||
" timestamp: msg.timestamp,\n"
|
||||
" duration: msg.duration || 10\n"
|
||||
" });\n"
|
||||
" \n"
|
||||
" // Проигрываем звук\n"
|
||||
" if (msg.sound && msg.sound !== '') {\n"
|
||||
" playSound(msg.sound);\n"
|
||||
" }\n"
|
||||
" \n"
|
||||
" // Запускаем таймер удаления\n"
|
||||
" setTimeout(() => {\n"
|
||||
" removeNotification(msgId);\n"
|
||||
" }, (msg.duration || 10) * 1000);\n"
|
||||
" });\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"function removeNotification(id) {\n"
|
||||
" if (notifications.has(id)) {\n"
|
||||
" const notification = notifications.get(id);\n"
|
||||
" notification.element.classList.add('fade-out');\n"
|
||||
" \n"
|
||||
" // Удаляем после анимации\n"
|
||||
" setTimeout(() => {\n"
|
||||
" if (notification.element.parentNode) {\n"
|
||||
" notification.element.parentNode.removeChild(notification.element);\n"
|
||||
" }\n"
|
||||
" notifications.delete(id);\n"
|
||||
" }, 1000);\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"function playSound(soundUrl) {\n"
|
||||
" if (!soundUrl) return;\n"
|
||||
" \n"
|
||||
" const audio = new Audio(soundUrl);\n"
|
||||
" audio.volume = 0.5;\n"
|
||||
" audio.play().catch(e => console.log('Audio error:', e));\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"// Загружаем при старте\n"
|
||||
"loadMessages();\n"
|
||||
"\n"
|
||||
"// Обновляем каждые 2 секунды (не каждый раз)\n"
|
||||
"setInterval(loadMessages, 2000);\n"
|
||||
"\n"
|
||||
"// Обновляем при возврате на вкладку\n"
|
||||
"document.addEventListener('visibilitychange', () => {\n"
|
||||
" if (!document.hidden) {\n"
|
||||
" loadMessages();\n"
|
||||
" }\n"
|
||||
"});\n"
|
||||
"</script>\n"
|
||||
"</body>\n"
|
||||
"</html>";
|
||||
|
||||
return html.arg(m_pageBackgroundColor); // Подставляем цвет фона
|
||||
}
|
||||
|
||||
QString HttpServer::generateJSON()
|
||||
{
|
||||
cleanOldMessages();
|
||||
|
||||
QJsonArray jsonArray;
|
||||
|
||||
|
||||
|
||||
for (const Notification ¬if : m_notifications) {
|
||||
QJsonObject obj;
|
||||
obj["nickname"] = notif.nickname;
|
||||
|
||||
// Исправляем пути - добавляем слеш в начале
|
||||
if (!notif.url.isEmpty()) {
|
||||
obj["url"] = notif.url.startsWith("/") ? notif.url : "/" + notif.url;
|
||||
} else {
|
||||
obj["url"] = "";
|
||||
}
|
||||
|
||||
obj["content"] = notif.content;
|
||||
obj["timestamp"] = notif.timestamp;
|
||||
|
||||
// Исправляем путь к звуку
|
||||
if (!notif.soundURL.isEmpty()) {
|
||||
obj["sound"] = notif.soundURL.startsWith("/") ? notif.soundURL : "/" + notif.soundURL;
|
||||
} else {
|
||||
obj["sound"] = "";
|
||||
}
|
||||
|
||||
obj["duration"] = notif.duration;
|
||||
obj["color"] = notif.blockColor;
|
||||
obj["borderColor"] = notif.borderColor;
|
||||
obj["borderSize"] = notif.borderSize;
|
||||
obj["titleColor"] = notif.titleColor;
|
||||
obj["titleFamily"] = notif.titleFamily;
|
||||
obj["titleSize"] = notif.titleSize;
|
||||
obj["contentColor"] = notif.contentColor;
|
||||
obj["contentFamily"] = notif.contentFamily;
|
||||
obj["contentSize"] = notif.contentSize;
|
||||
|
||||
jsonArray.append(obj);
|
||||
|
||||
|
||||
}
|
||||
|
||||
QJsonDocument doc(jsonArray);
|
||||
QString jsonStr = QString::fromUtf8(doc.toJson());
|
||||
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
void HttpServer::sendResponse(QTcpSocket *socket, const QString &contentType, const QString &content, int statusCode)
|
||||
{
|
||||
QString statusText;
|
||||
switch(statusCode) {
|
||||
case 200: statusText = "OK"; break;
|
||||
case 404: statusText = "Not Found"; break;
|
||||
default: statusText = "OK"; break;
|
||||
}
|
||||
|
||||
QByteArray responseData = QString(
|
||||
"HTTP/1.1 %1 %2\r\n"
|
||||
"Content-Type: %3\r\n"
|
||||
"Content-Length: %4\r\n"
|
||||
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"Connection: keep-alive\r\n"
|
||||
"\r\n"
|
||||
"%5")
|
||||
.arg(statusCode)
|
||||
.arg(statusText)
|
||||
.arg(contentType)
|
||||
.arg(content.toUtf8().size())
|
||||
.arg(content)
|
||||
.toUtf8();
|
||||
|
||||
socket->write(responseData);
|
||||
|
||||
// Не закрываем соединение сразу для keep-alive
|
||||
if (contentType == "text/html" || contentType.startsWith("application/json")) {
|
||||
// Для HTML и JSON оставляем соединение открытым
|
||||
socket->flush();
|
||||
} else {
|
||||
// Для статических файлов закрываем после отправки
|
||||
socket->flush();
|
||||
socket->close();
|
||||
}
|
||||
}
|
||||
|
||||
QString HttpServer::getMimeType(const QString &filePath)
|
||||
{
|
||||
if (filePath.endsWith(".html")) return "text/html";
|
||||
if (filePath.endsWith(".css")) return "text/css";
|
||||
if (filePath.endsWith(".js")) return "application/javascript";
|
||||
if (filePath.endsWith(".png")) return "image/png";
|
||||
if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (filePath.endsWith(".gif")) return "image/gif";
|
||||
if (filePath.endsWith(".svg")) return "image/svg+xml";
|
||||
if (filePath.endsWith(".ico")) return "image/x-icon";
|
||||
if (filePath.endsWith(".ttf")) return "font/ttf";
|
||||
if (filePath.endsWith(".otf")) return "font/otf";
|
||||
if (filePath.endsWith(".woff")) return "font/woff";
|
||||
if (filePath.endsWith(".woff2")) return "font/woff2";
|
||||
if (filePath.endsWith(".mp3")) return "audio/mpeg";
|
||||
if (filePath.endsWith(".wav")) return "audio/wav";
|
||||
if (filePath.endsWith(".ogg")) return "audio/ogg";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
void HttpServer::discardClient()
|
||||
{
|
||||
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
|
||||
if (socket) {
|
||||
m_clients.removeOne(socket);
|
||||
socket->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef WEBSERVERNOTIFY_H
|
||||
#define WEBSERVERNOTIFY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QMap>
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
#include <QList>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QDateTime>
|
||||
|
||||
struct Notification {
|
||||
QString nickname;
|
||||
QString url;
|
||||
QString content;
|
||||
QString soundURL;
|
||||
QString blockColor;
|
||||
QString borderColor;
|
||||
int borderSize;
|
||||
int duration;
|
||||
QString titleColor;
|
||||
QString titleFamily;
|
||||
int titleSize;
|
||||
QString contentColor;
|
||||
QString contentFamily;
|
||||
int contentSize;
|
||||
qint64 timestamp;
|
||||
|
||||
// Новое поле для цвета фона страницы
|
||||
QString pageBackgroundColor;
|
||||
|
||||
Notification() :
|
||||
borderSize(2),
|
||||
duration(10),
|
||||
titleSize(16),
|
||||
contentSize(14),
|
||||
timestamp(0),
|
||||
pageBackgroundColor("transparent") // По умолчанию прозрачный
|
||||
{}
|
||||
};
|
||||
|
||||
class HttpServer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HttpServer(QObject *parent = nullptr);
|
||||
~HttpServer();
|
||||
|
||||
bool start(quint16 port);
|
||||
void stop();
|
||||
quint16 port() const;
|
||||
|
||||
void addNotification(const Notification ¬ification);
|
||||
void clearNotifications();
|
||||
|
||||
signals:
|
||||
void serverStarted(bool success);
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
void readClient();
|
||||
void discardClient();
|
||||
|
||||
private:
|
||||
QTcpServer *m_server;
|
||||
QList<QTcpSocket*> m_clients;
|
||||
QList<Notification> m_notifications;
|
||||
QString m_pageBackgroundColor; // Цвет фона страницы
|
||||
|
||||
void processRequest(QTcpSocket *socket, const QString &request);
|
||||
void sendResponse(QTcpSocket *socket, const QString &contentType, const QString &content, int statusCode = 200);
|
||||
void sendHtmlPage(QTcpSocket *socket);
|
||||
void sendJSONMessages(QTcpSocket *socket);
|
||||
void serveStaticFile(QTcpSocket *socket, const QString &filePath);
|
||||
QString getMimeType(const QString &filePath);
|
||||
QString generateHTML();
|
||||
QString generateJSON();
|
||||
void cleanOldMessages();
|
||||
};
|
||||
|
||||
#endif // WEBSERVERNOTIFY_H
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "websocketclient.h"
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDateTime>
|
||||
#include <QCoreApplication>
|
||||
#include <synchapi.h>
|
||||
|
||||
WebSocketClient::WebSocketClient(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_webSocket(nullptr),
|
||||
m_isConnected(false)
|
||||
{
|
||||
m_pingTimer = new QTimer(this);
|
||||
m_pingTimer->setInterval(60000); // Ping каждые 30 секунд
|
||||
m_channelJoined = false;
|
||||
// Используем новый синтаксис подключения сигналов для Qt5
|
||||
connect(m_pingTimer, &QTimer::timeout, this, &WebSocketClient::onPingTimeout);
|
||||
}
|
||||
|
||||
WebSocketClient::~WebSocketClient()
|
||||
{
|
||||
disconnectFromServer();
|
||||
delete m_webSocket;
|
||||
}
|
||||
|
||||
bool WebSocketClient::connectToServer(const QString &url)
|
||||
{
|
||||
if (m_webSocket) {
|
||||
disconnectFromServer();
|
||||
delete m_webSocket;
|
||||
m_webSocket = nullptr;
|
||||
}
|
||||
|
||||
m_webSocket = new QWebSocket();
|
||||
|
||||
// Подключаем сигналы с новым синтаксисом Qt5
|
||||
connect(m_webSocket, &QWebSocket::connected,
|
||||
this, &WebSocketClient::onConnectedInternal);
|
||||
connect(m_webSocket, &QWebSocket::disconnected,
|
||||
this, &WebSocketClient::onDisconnectedInternal);
|
||||
connect(m_webSocket, &QWebSocket::textMessageReceived,
|
||||
this, &WebSocketClient::onTextMessageReceived);
|
||||
connect(m_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
|
||||
this, &WebSocketClient::onErrorInternal);
|
||||
connect(m_webSocket, &QWebSocket::sslErrors,
|
||||
this, &WebSocketClient::onSslErrors);
|
||||
|
||||
m_webSocket->open(QUrl(url));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketClient::connectToTwitchChat(const QString &oauthToken,
|
||||
const QString &nickname,
|
||||
const QString &channel)
|
||||
{
|
||||
m_nickname = nickname;
|
||||
m_currentChannel = channel;
|
||||
m_oauthToken = oauthToken;
|
||||
|
||||
// URL для подключения к Twitch IRC через WebSocket
|
||||
QString url = "wss://irc-ws.chat.twitch.tv:443";
|
||||
|
||||
connectToServer(url);
|
||||
}
|
||||
|
||||
void WebSocketClient::onConnectedInternal()
|
||||
{
|
||||
m_isConnected = true;
|
||||
m_pingTimer->start();
|
||||
|
||||
|
||||
// Аутентификация в Twitch
|
||||
authenticate(m_oauthToken, m_nickname);
|
||||
|
||||
emit onConnected();
|
||||
}
|
||||
|
||||
void WebSocketClient::onDisconnectedInternal()
|
||||
{
|
||||
m_isConnected = false;
|
||||
m_pingTimer->stop();
|
||||
m_channelJoined = false;
|
||||
emit onDisconnected();
|
||||
}
|
||||
|
||||
void WebSocketClient::send(const QString &data)
|
||||
{
|
||||
if (m_webSocket && m_isConnected) {
|
||||
m_webSocket->sendTextMessage(data);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::sendMessage(const QString &message)
|
||||
{
|
||||
send(message);
|
||||
}
|
||||
|
||||
void WebSocketClient::sendIrcCommand(const QString &command, const QString ¶meters)
|
||||
{
|
||||
QString message = command;
|
||||
if (!parameters.isEmpty()) {
|
||||
message += " " + parameters;
|
||||
}
|
||||
message += "\r\n";
|
||||
send(message);
|
||||
}
|
||||
|
||||
void WebSocketClient::authenticate(const QString &oauthToken, const QString &nickname)
|
||||
{
|
||||
|
||||
// PASS команда для аутентификации
|
||||
if (!oauthToken.startsWith("oauth:")) {
|
||||
sendIrcCommand("PASS", "oauth:" + oauthToken);
|
||||
} else {
|
||||
sendIrcCommand("PASS", oauthToken);
|
||||
}
|
||||
Sleep(1000);
|
||||
// NICK команда
|
||||
sendIrcCommand("NICK", nickname);
|
||||
|
||||
// CAP REQ для получения дополнительных возможностей
|
||||
sendIrcCommand("CAP REQ", ":twitch.tv/membership twitch.tv/tags twitch.tv/commands");
|
||||
}
|
||||
|
||||
void WebSocketClient::joinChannel(const QString &channel)
|
||||
{
|
||||
if (m_isConnected) {
|
||||
m_channelJoined = true;
|
||||
sendIrcCommand("JOIN", "#" + channel.toLower());
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::sendChatMessage(const QString &channel, const QString &message)
|
||||
{
|
||||
if (m_isConnected) {
|
||||
sendIrcCommand("PRIVMSG", "#" + channel.toLower() + " :" + message);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::pingServer()
|
||||
{
|
||||
if (m_isConnected) {
|
||||
send("PING :tmi.twitch.tv\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::onPingTimeout()
|
||||
{
|
||||
pingServer();
|
||||
}
|
||||
|
||||
void WebSocketClient::onTextMessageReceived(const QString &message)
|
||||
{
|
||||
|
||||
// Обработка PING от сервера
|
||||
if (message.startsWith("PING")) {
|
||||
send("PONG :tmi.twitch.tv\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка успешной аутентификации
|
||||
if (message.contains("001") && !m_currentChannel.isEmpty()) {
|
||||
joinChannel(m_currentChannel);
|
||||
return; // Добавить return после первого успешного подключения
|
||||
}
|
||||
|
||||
// Обработка сообщений чата
|
||||
if (message.contains("PRIVMSG")) {
|
||||
// Извлекаем информацию из сообщения
|
||||
// Формат: :nick!nick@nick.tmi.twitch.tv PRIVMSG #channel :message
|
||||
QString cleanMessage = message;
|
||||
emit onNewMessage(cleanMessage);
|
||||
// Удаляем служебную информацию для чистого отображения
|
||||
int msgIndex = cleanMessage.indexOf("PRIVMSG");
|
||||
if (msgIndex != -1) {
|
||||
int channelIndex = cleanMessage.indexOf("#", msgIndex);
|
||||
int messageIndex = cleanMessage.indexOf(" :", channelIndex);
|
||||
if (messageIndex != -1) {
|
||||
QString chatMessage = cleanMessage.mid(messageIndex + 2).trimmed();
|
||||
QString sender = cleanMessage.mid(1, cleanMessage.indexOf("!") - 1);
|
||||
QString channel = cleanMessage.mid(channelIndex + 1,
|
||||
messageIndex - channelIndex - 1);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void WebSocketClient::onErrorInternal(QAbstractSocket::SocketError error)
|
||||
{
|
||||
QString errorMsg = QString("WebSocket error: %1 - %2")
|
||||
.arg(error)
|
||||
.arg(m_webSocket ? m_webSocket->errorString() : "Unknown error");
|
||||
|
||||
|
||||
emit onError(errorMsg);
|
||||
}
|
||||
|
||||
void WebSocketClient::onSslErrors(const QList<QSslError> &errors)
|
||||
{
|
||||
QStringList errorMessages;
|
||||
foreach (const QSslError &error, errors) {
|
||||
errorMessages.append(error.errorString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// В разработке можно игнорировать SSL ошибки, но в продакшене это опасно
|
||||
if (m_webSocket) {
|
||||
m_webSocket->ignoreSslErrors();
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::disconnectFromServer()
|
||||
{
|
||||
if (m_webSocket && m_isConnected) {
|
||||
// Отправляем команду выхода из канала для Twitch
|
||||
if (!m_currentChannel.isEmpty()) {
|
||||
sendIrcCommand("PART", "#" + m_currentChannel.toLower());
|
||||
}
|
||||
|
||||
|
||||
m_webSocket->close();
|
||||
m_isConnected = false;
|
||||
m_pingTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool WebSocketClient::isConnected() const
|
||||
{
|
||||
return m_isConnected;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef WEBSOCKETCLIENT_H
|
||||
#define WEBSOCKETCLIENT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QWebSocket>
|
||||
#include <QUrl>
|
||||
#include <QTimer>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class WebSocketClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WebSocketClient(QObject *parent = nullptr);
|
||||
~WebSocketClient();
|
||||
|
||||
// Внешние методы
|
||||
Q_INVOKABLE bool connectToServer(const QString &url);
|
||||
Q_INVOKABLE void disconnectFromServer();
|
||||
Q_INVOKABLE void sendMessage(const QString &message);
|
||||
Q_INVOKABLE bool isConnected() const;
|
||||
|
||||
// Методы для Twitch чата
|
||||
Q_INVOKABLE void connectToTwitchChat(const QString &oauthToken, const QString &nickname, const QString &channel);
|
||||
Q_INVOKABLE void joinChannel(const QString &channel);
|
||||
Q_INVOKABLE void sendChatMessage(const QString &channel, const QString &message);
|
||||
|
||||
signals:
|
||||
// Внешние сигналы
|
||||
void onNewMessage(const QString &message);
|
||||
void onConnected();
|
||||
void onDisconnected();
|
||||
void onError(const QString &errorMessage);
|
||||
|
||||
private slots:
|
||||
// Внутренние слоты
|
||||
void onConnectedInternal();
|
||||
void onDisconnectedInternal();
|
||||
void onTextMessageReceived(const QString &message);
|
||||
void onErrorInternal(QAbstractSocket::SocketError error);
|
||||
void onSslErrors(const QList<QSslError> &errors);
|
||||
void onPingTimeout();
|
||||
void pingServer();
|
||||
private:
|
||||
// Внутренние методы
|
||||
void send(const QString &data);
|
||||
void sendIrcCommand(const QString &command, const QString ¶meters = "");
|
||||
void authenticate(const QString &oauthToken, const QString &nickname);
|
||||
|
||||
private:
|
||||
QWebSocket *m_webSocket;
|
||||
QTimer *m_pingTimer;
|
||||
bool m_isConnected;
|
||||
QString m_nickname;
|
||||
QString m_currentChannel;
|
||||
QString m_oauthToken;
|
||||
bool m_channelJoined;
|
||||
};
|
||||
|
||||
#endif // WEBSOCKETCLIENT_H
|
||||