完成了OTA功能,为游戏增加了图标
This commit is contained in:
+61
-2
@@ -11,7 +11,6 @@ if (NOT DEFINED CMAKE_PREFIX_PATH)
|
|||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(CMAKE_PREFIX_PATH "E:/QT6.6.3/6.6.3/mingw_64")
|
set(CMAKE_PREFIX_PATH "E:/QT6.6.3/6.6.3/mingw_64")
|
||||||
else ()
|
else ()
|
||||||
# Linux/Qt 官方安装器默认路径示例,按你实际安装位置修改
|
|
||||||
set(CMAKE_PREFIX_PATH "$ENV{HOME}/Qt6.3/6.6.3/gcc_64")
|
set(CMAKE_PREFIX_PATH "$ENV{HOME}/Qt6.3/6.6.3/gcc_64")
|
||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
@@ -42,7 +41,15 @@ set(APP_SOURCES
|
|||||||
resources.qrc
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(Embalming_Girl ${APP_SOURCES})
|
if (WIN32)
|
||||||
|
list(APPEND APP_SOURCES app.rc)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||||
|
add_executable(Embalming_Girl WIN32 ${APP_SOURCES})
|
||||||
|
else ()
|
||||||
|
add_executable(Embalming_Girl ${APP_SOURCES})
|
||||||
|
endif ()
|
||||||
|
|
||||||
target_include_directories(Embalming_Girl PRIVATE
|
target_include_directories(Embalming_Girl PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
@@ -50,6 +57,7 @@ target_include_directories(Embalming_Girl PRIVATE
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/network
|
${CMAKE_CURRENT_SOURCE_DIR}/src/network
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/ui
|
${CMAKE_CURRENT_SOURCE_DIR}/src/ui
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/voice
|
${CMAKE_CURRENT_SOURCE_DIR}/src/voice
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/update
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(Embalming_Girl
|
target_link_libraries(Embalming_Girl
|
||||||
@@ -116,3 +124,54 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
|||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/Res"
|
"${CMAKE_CURRENT_SOURCE_DIR}/Res"
|
||||||
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/Res")
|
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/Res")
|
||||||
|
|
||||||
|
# ============ Distribution packaging target ============
|
||||||
|
if (WIN32)
|
||||||
|
set(DEPLOY_DIR "${CMAKE_BINARY_DIR}/deploy/Embalming_Girl")
|
||||||
|
|
||||||
|
add_custom_target(deploy
|
||||||
|
DEPENDS ${PROJECT_NAME}
|
||||||
|
COMMENT "=== Creating distribution package ==="
|
||||||
|
|
||||||
|
# Clean and create directory
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E remove_directory "${DEPLOY_DIR}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_DIR}"
|
||||||
|
|
||||||
|
# Copy main executable
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:${PROJECT_NAME}>" "${DEPLOY_DIR}/"
|
||||||
|
|
||||||
|
# Copy ElaWidgetTools DLL
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:ElaWidgetTools>" "${DEPLOY_DIR}/"
|
||||||
|
|
||||||
|
# Run windeployqt to copy Qt DLLs + plugins + compiler runtime
|
||||||
|
COMMAND "${QT_INSTALL_PATH}/bin/windeployqt.exe"
|
||||||
|
--no-translations
|
||||||
|
--no-system-d3d-compiler
|
||||||
|
--no-opengl-sw
|
||||||
|
"$<$<CONFIG:Debug>:--debug>$<$<CONFIG:Release>:--release>"
|
||||||
|
"${DEPLOY_DIR}/$<TARGET_FILE_NAME:${PROJECT_NAME}>"
|
||||||
|
|
||||||
|
# Copy multimedia plugins (windeployqt may miss these)
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_DIR}/plugins/multimedia"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${QT_INSTALL_PATH}/plugins/multimedia"
|
||||||
|
"${DEPLOY_DIR}/plugins/multimedia"
|
||||||
|
|
||||||
|
# Copy game resources
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/Res"
|
||||||
|
"${DEPLOY_DIR}/Res"
|
||||||
|
|
||||||
|
# Copy config
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/config"
|
||||||
|
"${DEPLOY_DIR}/config"
|
||||||
|
|
||||||
|
# Copy updater.exe (built from Go server project)
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/../Embalming_Girl_Updater/updater.exe"
|
||||||
|
"${DEPLOY_DIR}/updater.exe"
|
||||||
|
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E echo "=== Package created at: ${DEPLOY_DIR} ==="
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -1,4 +1,9 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QMutex>
|
||||||
|
|
||||||
#include "ElaApplication.h"
|
#include "ElaApplication.h"
|
||||||
#include "ElaTheme.h"
|
#include "ElaTheme.h"
|
||||||
@@ -6,10 +11,50 @@
|
|||||||
#include "CardData.h"
|
#include "CardData.h"
|
||||||
#include "SettingsManager.h"
|
#include "SettingsManager.h"
|
||||||
#include "SoundManager.h"
|
#include "SoundManager.h"
|
||||||
|
#include "UpdateChecker.h"
|
||||||
|
|
||||||
|
static QFile* g_logFile = nullptr;
|
||||||
|
static QMutex g_logMutex;
|
||||||
|
|
||||||
|
void messageHandler(QtMsgType type, const QMessageLogContext& ctx, const QString& msg) {
|
||||||
|
Q_UNUSED(ctx)
|
||||||
|
QMutexLocker lock(&g_logMutex);
|
||||||
|
QString level;
|
||||||
|
switch (type) {
|
||||||
|
case QtDebugMsg: level = "DEBUG"; break;
|
||||||
|
case QtInfoMsg: level = "INFO"; break;
|
||||||
|
case QtWarningMsg: level = "WARN"; break;
|
||||||
|
case QtCriticalMsg: level = "ERROR"; break;
|
||||||
|
case QtFatalMsg: level = "FATAL"; break;
|
||||||
|
}
|
||||||
|
QString line = QStringLiteral("[%1] [%2] %3\n")
|
||||||
|
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz"), level, msg);
|
||||||
|
#ifndef QT_NO_DEBUG_OUTPUT
|
||||||
|
fprintf(stderr, "%s", line.toLocal8Bit().constData());
|
||||||
|
#endif
|
||||||
|
if (g_logFile && g_logFile->isOpen())
|
||||||
|
g_logFile->write(line.toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
|
a.setApplicationVersion(APP_VERSION);
|
||||||
|
|
||||||
|
QString logDir = QCoreApplication::applicationDirPath() + "/logs";
|
||||||
|
QDir().mkpath(logDir);
|
||||||
|
QString logPath = logDir + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd_HHmmss") + ".log";
|
||||||
|
g_logFile = new QFile(logPath);
|
||||||
|
g_logFile->open(QIODevice::WriteOnly | QIODevice::Append);
|
||||||
|
qInstallMessageHandler(messageHandler);
|
||||||
|
|
||||||
|
QDir logClean(logDir);
|
||||||
|
for (const auto& f : logClean.entryList({"*.log"}, QDir::Files)) {
|
||||||
|
QFileInfo fi(logDir + "/" + f);
|
||||||
|
if (fi.lastModified().daysTo(QDateTime::currentDateTime()) > 7)
|
||||||
|
QFile::remove(fi.absoluteFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
eApp->init();
|
eApp->init();
|
||||||
eTheme->setThemeMode(ElaThemeType::Dark);
|
eTheme->setThemeMode(ElaThemeType::Dark);
|
||||||
|
|
||||||
@@ -22,5 +67,11 @@ int main(int argc, char* argv[])
|
|||||||
MainWindow w;
|
MainWindow w;
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
|
QTimer::singleShot(2000, [&]() {
|
||||||
|
auto server = SettingsManager::instance().getString("network", "server_address");
|
||||||
|
if (!server.isEmpty())
|
||||||
|
UpdateChecker::instance().checkForUpdate(server, true);
|
||||||
|
});
|
||||||
|
|
||||||
return QApplication::exec();
|
return QApplication::exec();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/images">
|
<qresource prefix="/images">
|
||||||
|
<file alias="app_icon">Res/Icon/Em.png</file>
|
||||||
<file alias="card_back">Res/卡牌背景.png</file>
|
<file alias="card_back">Res/卡牌背景.png</file>
|
||||||
<file alias="corpse">Res/少女的尸体.png</file>
|
<file alias="corpse">Res/少女的尸体.png</file>
|
||||||
<file alias="harmony_target">Res/调和目标.png</file>
|
<file alias="harmony_target">Res/调和目标.png</file>
|
||||||
|
|||||||
@@ -13,10 +13,20 @@ NetworkManager::NetworkManager(QObject* parent)
|
|||||||
emit binaryFrameReceived(msg);
|
emit binaryFrameReceived(msg);
|
||||||
});
|
});
|
||||||
connect(&_heartbeatTimer, &QTimer::timeout, this, &NetworkManager::onHeartbeat);
|
connect(&_heartbeatTimer, &QTimer::timeout, this, &NetworkManager::onHeartbeat);
|
||||||
|
_reconnectTimer.setSingleShot(true);
|
||||||
|
connect(&_reconnectTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
if (!_autoReconnect || _serverUrl.isEmpty()) return;
|
||||||
|
_reconnectAttempts++;
|
||||||
|
emit reconnecting(_reconnectAttempts, 10);
|
||||||
|
_socket.open(QUrl(_serverUrl));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::connectToServer(const QString& url) {
|
void NetworkManager::connectToServer(const QString& url) {
|
||||||
_serverUrl = url;
|
_serverUrl = url;
|
||||||
|
_autoReconnect = true;
|
||||||
|
_reconnectAttempts = 0;
|
||||||
|
_reconnectTimer.stop();
|
||||||
_socket.open(QUrl(url));
|
_socket.open(QUrl(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +35,14 @@ void NetworkManager::disconnect() {
|
|||||||
_socket.close();
|
_socket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkManager::disconnectPermanent() {
|
||||||
|
_autoReconnect = false;
|
||||||
|
_reconnectTimer.stop();
|
||||||
|
_reconnectAttempts = 0;
|
||||||
|
_heartbeatTimer.stop();
|
||||||
|
_socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
bool NetworkManager::isConnected() const {
|
bool NetworkManager::isConnected() const {
|
||||||
return _socket.state() == QAbstractSocket::ConnectedState;
|
return _socket.state() == QAbstractSocket::ConnectedState;
|
||||||
}
|
}
|
||||||
@@ -103,6 +121,8 @@ void NetworkManager::sendBinaryFrame(const QByteArray& data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::onConnected() {
|
void NetworkManager::onConnected() {
|
||||||
|
_reconnectAttempts = 0;
|
||||||
|
_reconnectTimer.stop();
|
||||||
_heartbeatTimer.start(15000);
|
_heartbeatTimer.start(15000);
|
||||||
emit connected();
|
emit connected();
|
||||||
}
|
}
|
||||||
@@ -110,6 +130,10 @@ void NetworkManager::onConnected() {
|
|||||||
void NetworkManager::onDisconnected() {
|
void NetworkManager::onDisconnected() {
|
||||||
_heartbeatTimer.stop();
|
_heartbeatTimer.stop();
|
||||||
emit disconnected();
|
emit disconnected();
|
||||||
|
if (_autoReconnect && _reconnectAttempts < 10 && !_serverUrl.isEmpty()) {
|
||||||
|
int delay = qMin(3000 + _reconnectAttempts * 2000, 20000);
|
||||||
|
_reconnectTimer.start(delay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::onTextMessageReceived(const QString& message) {
|
void NetworkManager::onTextMessageReceived(const QString& message) {
|
||||||
@@ -167,6 +191,8 @@ void NetworkManager::handleServerMessage(const QJsonObject& msg) {
|
|||||||
emit settlementResult(payload);
|
emit settlementResult(payload);
|
||||||
} else if (type == "game_log") {
|
} else if (type == "game_log") {
|
||||||
emit gameLogReceived(payload["text"].toString());
|
emit gameLogReceived(payload["text"].toString());
|
||||||
|
} else if (type == "player_disconnected" || type == "player_bot_takeover") {
|
||||||
|
emit gameLogReceived(payload["message"].toString());
|
||||||
} else if (type == "login_result") {
|
} else if (type == "login_result") {
|
||||||
emit loginResult(payload);
|
emit loginResult(payload);
|
||||||
} else if (type == "register_result") {
|
} else if (type == "register_result") {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public:
|
|||||||
|
|
||||||
void connectToServer(const QString& url);
|
void connectToServer(const QString& url);
|
||||||
void disconnect();
|
void disconnect();
|
||||||
|
void disconnectPermanent();
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
|
|
||||||
void sendMessage(const QString& type, const QJsonObject& payload = {});
|
void sendMessage(const QString& type, const QJsonObject& payload = {});
|
||||||
@@ -38,6 +39,7 @@ signals:
|
|||||||
void connected();
|
void connected();
|
||||||
void disconnected();
|
void disconnected();
|
||||||
void connectionError(const QString& error);
|
void connectionError(const QString& error);
|
||||||
|
void reconnecting(int attempt, int maxAttempts);
|
||||||
|
|
||||||
void roomStateUpdated(const QJsonObject& roomState);
|
void roomStateUpdated(const QJsonObject& roomState);
|
||||||
void gameStarted(const QJsonObject& initConfig);
|
void gameStarted(const QJsonObject& initConfig);
|
||||||
@@ -77,7 +79,10 @@ private:
|
|||||||
|
|
||||||
QWebSocket _socket;
|
QWebSocket _socket;
|
||||||
QTimer _heartbeatTimer;
|
QTimer _heartbeatTimer;
|
||||||
|
QTimer _reconnectTimer;
|
||||||
int _seqNum = 0;
|
int _seqNum = 0;
|
||||||
|
int _reconnectAttempts = 0;
|
||||||
|
bool _autoReconnect = false;
|
||||||
QString _serverUrl;
|
QString _serverUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+110
-3
@@ -28,6 +28,7 @@
|
|||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
#include "SettingsDialog.h"
|
#include "SettingsDialog.h"
|
||||||
|
#include "SettingsManager.h"
|
||||||
#include "SoundManager.h"
|
#include "SoundManager.h"
|
||||||
#include "SpectrumWidget.h"
|
#include "SpectrumWidget.h"
|
||||||
#include <QGraphicsPixmapItem>
|
#include <QGraphicsPixmapItem>
|
||||||
@@ -67,6 +68,14 @@ GameWidget::GameWidget(QWidget* parent) : QWidget(parent)
|
|||||||
});
|
});
|
||||||
connect(&net, &NetworkManager::gameLogReceived, this, [this](const QString& text) {
|
connect(&net, &NetworkManager::gameLogReceived, this, [this](const QString& text) {
|
||||||
addLog(text);
|
addLog(text);
|
||||||
|
if (text.contains(QStringLiteral("特技")))
|
||||||
|
SoundManager::instance().play("action_skill");
|
||||||
|
else if (text.contains(QStringLiteral("调和区")))
|
||||||
|
SoundManager::instance().play("action_harmony");
|
||||||
|
else if (text.contains(QStringLiteral("质疑了")))
|
||||||
|
SoundManager::instance().play("action_challenge");
|
||||||
|
else if (text.contains(QStringLiteral("退出")))
|
||||||
|
SoundManager::instance().play("player_exit");
|
||||||
});
|
});
|
||||||
connect(&net, &NetworkManager::chatMessage, this, [this](const QJsonObject& m) {
|
connect(&net, &NetworkManager::chatMessage, this, [this](const QJsonObject& m) {
|
||||||
auto nick = m["nickname"].toString();
|
auto nick = m["nickname"].toString();
|
||||||
@@ -248,6 +257,36 @@ void GameWidget::initFloatingUI() {
|
|||||||
_playAnimLabel->setAlignment(Qt::AlignCenter);
|
_playAnimLabel->setAlignment(Qt::AlignCenter);
|
||||||
paLay->addWidget(_playAnimLabel, 0, Qt::AlignCenter);
|
paLay->addWidget(_playAnimLabel, 0, Qt::AlignCenter);
|
||||||
|
|
||||||
|
_disconnectOverlay = new QWidget(this);
|
||||||
|
_disconnectOverlay->hide();
|
||||||
|
_disconnectOverlay->setStyleSheet("background:rgba(0,0,0,200);");
|
||||||
|
auto* dcLay = new QVBoxLayout(_disconnectOverlay);
|
||||||
|
dcLay->setAlignment(Qt::AlignCenter);
|
||||||
|
_disconnectLabel = new QLabel(QStringLiteral("连接断开,正在重连..."), _disconnectOverlay);
|
||||||
|
_disconnectLabel->setStyleSheet("color:#d94f5c;font-size:18px;font-weight:bold;background:transparent;");
|
||||||
|
_disconnectLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
dcLay->addWidget(_disconnectLabel);
|
||||||
|
|
||||||
|
connect(&NetworkManager::instance(), &NetworkManager::disconnected, this, [this]() {
|
||||||
|
_disconnectOverlay->setGeometry(rect());
|
||||||
|
_disconnectOverlay->show();
|
||||||
|
_disconnectOverlay->raise();
|
||||||
|
});
|
||||||
|
connect(&NetworkManager::instance(), &NetworkManager::reconnecting, this, [this](int attempt, int max) {
|
||||||
|
_disconnectLabel->setText(QStringLiteral("连接断开,正在重连... (%1/%2)").arg(attempt).arg(max));
|
||||||
|
});
|
||||||
|
connect(&NetworkManager::instance(), &NetworkManager::connected, this, [this]() {
|
||||||
|
bool wasDisconnected = _disconnectOverlay->isVisible();
|
||||||
|
_disconnectOverlay->hide();
|
||||||
|
if (wasDisconnected) {
|
||||||
|
auto token = SettingsManager::instance().getString("account", "token");
|
||||||
|
if (!token.isEmpty()) {
|
||||||
|
QJsonObject p; p["token"] = token;
|
||||||
|
NetworkManager::instance().sendMessage("login", p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_chatPanel = new QWidget(this);
|
_chatPanel = new QWidget(this);
|
||||||
_chatPanel->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.7);border-right:1px solid rgba(94,179,230,0.15);"));
|
_chatPanel->setStyleSheet(QStringLiteral("background:rgba(0,0,0,0.7);border-right:1px solid rgba(94,179,230,0.15);"));
|
||||||
auto* chatLay = new QVBoxLayout(_chatPanel);
|
auto* chatLay = new QVBoxLayout(_chatPanel);
|
||||||
@@ -343,6 +382,7 @@ void GameWidget::layoutFloatingUI() {
|
|||||||
_settlementOverlay->setGeometry(0, 0, w, h);
|
_settlementOverlay->setGeometry(0, 0, w, h);
|
||||||
_zoomOverlay->setGeometry(0, 0, w, h);
|
_zoomOverlay->setGeometry(0, 0, w, h);
|
||||||
_playAnimWidget->setGeometry(0, 0, w, h);
|
_playAnimWidget->setGeometry(0, 0, w, h);
|
||||||
|
_disconnectOverlay->setGeometry(0, 0, w, h);
|
||||||
_zoomCard->move((w - _zoomCard->width()) / 2, (h - _zoomCard->height()) / 2);
|
_zoomCard->move((w - _zoomCard->width()) / 2, (h - _zoomCard->height()) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +406,10 @@ void GameWidget::onGameStart(const QJsonObject& config) {
|
|||||||
_turnLabel->setText(QStringLiteral("游戏开始!"));
|
_turnLabel->setText(QStringLiteral("游戏开始!"));
|
||||||
_harmonyLabel->setPlainText(QStringLiteral("调和区 (目标: %1)").arg(_harmonyTarget));
|
_harmonyLabel->setPlainText(QStringLiteral("调和区 (目标: %1)").arg(_harmonyTarget));
|
||||||
addLog(QStringLiteral("游戏开始 - 调和目标值 %1").arg(_harmonyTarget));
|
addLog(QStringLiteral("游戏开始 - 调和目标值 %1").arg(_harmonyTarget));
|
||||||
|
QTimer::singleShot(100, this, [this]() {
|
||||||
|
layoutFloatingUI();
|
||||||
|
layoutPlayerSeats();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWidget::onSnapshot(const QJsonObject& snap) {
|
void GameWidget::onSnapshot(const QJsonObject& snap) {
|
||||||
@@ -386,6 +430,7 @@ void GameWidget::onSnapshot(const QJsonObject& snap) {
|
|||||||
if (!_seats.contains(pid)) {
|
if (!_seats.contains(pid)) {
|
||||||
auto* seat = new PlayerSeatWidget(this);
|
auto* seat = new PlayerSeatWidget(this);
|
||||||
connect(seat, &PlayerSeatWidget::playerClicked, this, &GameWidget::onPlayerSeatClicked);
|
connect(seat, &PlayerSeatWidget::playerClicked, this, &GameWidget::onPlayerSeatClicked);
|
||||||
|
connect(seat, &PlayerSeatWidget::skillCardZoom, this, &GameWidget::showCardZoom);
|
||||||
_seats[pid] = seat;
|
_seats[pid] = seat;
|
||||||
}
|
}
|
||||||
_seats[pid]->setPlayerData(po);
|
_seats[pid]->setPlayerData(po);
|
||||||
@@ -404,6 +449,67 @@ void GameWidget::onSnapshot(const QJsonObject& snap) {
|
|||||||
updateHandCards(snap["hand_cards"].toArray());
|
updateHandCards(snap["hand_cards"].toArray());
|
||||||
updateHarmonyZone(snap["harmony_zone"].toArray());
|
updateHarmonyZone(snap["harmony_zone"].toArray());
|
||||||
|
|
||||||
|
int roundNum = snap["round_num"].toInt(1);
|
||||||
|
_targetLabel->setText(QStringLiteral("第%1轮 | 目标值: %2").arg(roundNum).arg(_harmonyTarget));
|
||||||
|
|
||||||
|
for (auto* c : _mySkillCards) { _scene->removeItem(c); delete c; }
|
||||||
|
_mySkillCards.clear();
|
||||||
|
for (const auto& pv : players) {
|
||||||
|
auto po = pv.toObject();
|
||||||
|
if (po["player_id"].toString() == _localPlayerId) {
|
||||||
|
auto skills = po["skill_zone"].toArray();
|
||||||
|
QRect ga = gameArea();
|
||||||
|
int cx = ga.center().x();
|
||||||
|
qreal y = ga.y() + ga.height() * 0.46;
|
||||||
|
qreal w = SceneCardItem::W * 0.55;
|
||||||
|
qreal total = (skills.size() - 1) * (w + 4) + w;
|
||||||
|
qreal sx = cx - total / 2.0;
|
||||||
|
for (int i = 0; i < skills.size(); ++i) {
|
||||||
|
auto sc = skills[i].toObject();
|
||||||
|
auto* item = new SceneCardItem();
|
||||||
|
item->setCardData(sc["uid"].toString(), sc["type_id"].toString(), true);
|
||||||
|
item->setPos(sx + i * (w + 4), y);
|
||||||
|
item->setScale(0.55);
|
||||||
|
item->setCardEnabled(false);
|
||||||
|
item->setZValue(8);
|
||||||
|
connect(item, &SceneCardItem::rightClicked, this, &GameWidget::showCardZoom);
|
||||||
|
_scene->addItem(item);
|
||||||
|
_mySkillCards.append(item);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* c : _myChallengeItems) { _scene->removeItem(c); delete c; }
|
||||||
|
_myChallengeItems.clear();
|
||||||
|
auto myCZ = snap["my_challenge_zone"].toArray();
|
||||||
|
if (!myCZ.isEmpty()) {
|
||||||
|
QRect ga = gameArea();
|
||||||
|
int cx = ga.center().x();
|
||||||
|
qreal y = ga.y() + ga.height() * 0.58;
|
||||||
|
qreal w = SceneCardItem::W * 0.5;
|
||||||
|
qreal total = (myCZ.size() - 1) * (w + 8) + w;
|
||||||
|
qreal sx = cx - total / 2.0;
|
||||||
|
for (int i = 0; i < myCZ.size(); ++i) {
|
||||||
|
auto ce = myCZ[i].toObject();
|
||||||
|
auto* item = new SceneCardItem();
|
||||||
|
item->setCardData(ce["uid"].toString(), "", false);
|
||||||
|
item->setPos(sx + i * (w + 8), y);
|
||||||
|
item->setScale(0.5);
|
||||||
|
item->setCardEnabled(false);
|
||||||
|
item->setZValue(8);
|
||||||
|
_scene->addItem(item);
|
||||||
|
_myChallengeItems.append(item);
|
||||||
|
|
||||||
|
auto* label = _scene->addSimpleText(QStringLiteral("来自: ") + ce["placed_by"].toString(),
|
||||||
|
QFont(QString(), 8));
|
||||||
|
label->setBrush(QColor("#d94f5c"));
|
||||||
|
label->setPos(sx + i * (w + 8), y + SceneCardItem::H * 0.5 + 2);
|
||||||
|
label->setZValue(9);
|
||||||
|
_myChallengeItems.append(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (snap.contains("effect")) {
|
if (snap.contains("effect")) {
|
||||||
auto eInfo = snap["effect"].toObject();
|
auto eInfo = snap["effect"].toObject();
|
||||||
if (eInfo["needs_response"].toBool()) {
|
if (eInfo["needs_response"].toBool()) {
|
||||||
@@ -471,6 +577,10 @@ void GameWidget::resetGame() {
|
|||||||
_handCards.clear();
|
_handCards.clear();
|
||||||
for (auto* c : _harmonyCards) { _scene->removeItem(c); delete c; }
|
for (auto* c : _harmonyCards) { _scene->removeItem(c); delete c; }
|
||||||
_harmonyCards.clear();
|
_harmonyCards.clear();
|
||||||
|
for (auto* c : _mySkillCards) { _scene->removeItem(c); delete c; }
|
||||||
|
_mySkillCards.clear();
|
||||||
|
for (auto* c : _myChallengeItems) { _scene->removeItem(c); delete c; }
|
||||||
|
_myChallengeItems.clear();
|
||||||
for (auto* s : _seats) s->deleteLater();
|
for (auto* s : _seats) s->deleteLater();
|
||||||
_seats.clear();
|
_seats.clear();
|
||||||
_playerOrder.clear();
|
_playerOrder.clear();
|
||||||
@@ -623,7 +733,6 @@ void GameWidget::onSkill() {
|
|||||||
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
|
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
|
||||||
const CardDef* def = CardDatabase::instance().getCardDef(typeId);
|
const CardDef* def = CardDatabase::instance().getCardDef(typeId);
|
||||||
showPlayAnimation(typeId, QStringLiteral("特技: ") + (def ? def->name : typeId));
|
showPlayAnimation(typeId, QStringLiteral("特技: ") + (def ? def->name : typeId));
|
||||||
SoundManager::instance().play("action_skill");
|
|
||||||
NetworkManager::instance().selectCard(_selectedCardUid);
|
NetworkManager::instance().selectCard(_selectedCardUid);
|
||||||
NetworkManager::instance().confirmAction("skill");
|
NetworkManager::instance().confirmAction("skill");
|
||||||
_phase = Phase::WaitResponse; setActionsEnabled(false,false,false);
|
_phase = Phase::WaitResponse; setActionsEnabled(false,false,false);
|
||||||
@@ -634,7 +743,6 @@ void GameWidget::onHarmony() {
|
|||||||
QString typeId;
|
QString typeId;
|
||||||
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
|
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
|
||||||
showPlayAnimation(typeId, QStringLiteral("调和"));
|
showPlayAnimation(typeId, QStringLiteral("调和"));
|
||||||
SoundManager::instance().play("action_harmony");
|
|
||||||
NetworkManager::instance().selectCard(_selectedCardUid);
|
NetworkManager::instance().selectCard(_selectedCardUid);
|
||||||
NetworkManager::instance().confirmAction("harmony");
|
NetworkManager::instance().confirmAction("harmony");
|
||||||
_phase = Phase::WaitResponse; setActionsEnabled(false,false,false);
|
_phase = Phase::WaitResponse; setActionsEnabled(false,false,false);
|
||||||
@@ -655,7 +763,6 @@ void GameWidget::onPlayerSeatClicked(const QString& playerId) {
|
|||||||
QString typeId;
|
QString typeId;
|
||||||
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
|
for (auto* c : _handCards) if (c->uid() == _selectedCardUid) typeId = c->typeId();
|
||||||
showPlayAnimation(typeId, QStringLiteral("质疑"));
|
showPlayAnimation(typeId, QStringLiteral("质疑"));
|
||||||
SoundManager::instance().play("action_challenge");
|
|
||||||
NetworkManager::instance().selectCard(_selectedCardUid);
|
NetworkManager::instance().selectCard(_selectedCardUid);
|
||||||
QJsonObject extra;
|
QJsonObject extra;
|
||||||
extra["target_player_id"] = playerId;
|
extra["target_player_id"] = playerId;
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ private:
|
|||||||
|
|
||||||
QVector<SceneCardItem*> _handCards;
|
QVector<SceneCardItem*> _handCards;
|
||||||
QVector<SceneCardItem*> _harmonyCards;
|
QVector<SceneCardItem*> _harmonyCards;
|
||||||
|
QVector<SceneCardItem*> _mySkillCards;
|
||||||
|
QVector<QGraphicsItem*> _myChallengeItems;
|
||||||
QMap<QString, PlayerSeatWidget*> _seats;
|
QMap<QString, PlayerSeatWidget*> _seats;
|
||||||
QVector<QString> _playerOrder;
|
QVector<QString> _playerOrder;
|
||||||
|
|
||||||
@@ -113,6 +115,8 @@ private:
|
|||||||
QWidget* _playAnimWidget = nullptr;
|
QWidget* _playAnimWidget = nullptr;
|
||||||
CardWidget* _playAnimCard = nullptr;
|
CardWidget* _playAnimCard = nullptr;
|
||||||
QLabel* _playAnimLabel = nullptr;
|
QLabel* _playAnimLabel = nullptr;
|
||||||
|
QWidget* _disconnectOverlay = nullptr;
|
||||||
|
QLabel* _disconnectLabel = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+10
-7
@@ -7,6 +7,7 @@
|
|||||||
#include "ElaMessageBar.h"
|
#include "ElaMessageBar.h"
|
||||||
#include "NetworkManager.h"
|
#include "NetworkManager.h"
|
||||||
#include "SettingsManager.h"
|
#include "SettingsManager.h"
|
||||||
|
#include "SettingsDialog.h"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
@@ -82,17 +83,21 @@ void LobbyWidget::initUI() {
|
|||||||
connect(profileBtn, &ElaPushButton::clicked, this, [this]() { emit profileRequested(); });
|
connect(profileBtn, &ElaPushButton::clicked, this, [this]() { emit profileRequested(); });
|
||||||
headerRow->addWidget(profileBtn);
|
headerRow->addWidget(profileBtn);
|
||||||
headerRow->addStretch();
|
headerRow->addStretch();
|
||||||
|
auto* settingsBtn = new ElaPushButton(QStringLiteral("⚙"), card);
|
||||||
|
settingsBtn->setFixedSize(30, 26);
|
||||||
|
connect(settingsBtn, &ElaPushButton::clicked, this, [this]() { SettingsDialog dlg(this); dlg.exec(); });
|
||||||
|
headerRow->addWidget(settingsBtn);
|
||||||
auto* logoutBtn = new ElaPushButton(QStringLiteral("登出"), card);
|
auto* logoutBtn = new ElaPushButton(QStringLiteral("登出"), card);
|
||||||
logoutBtn->setFixedSize(60, 26);
|
logoutBtn->setFixedSize(60, 26);
|
||||||
connect(logoutBtn, &ElaPushButton::clicked, this, [this]() {
|
connect(logoutBtn, &ElaPushButton::clicked, this, [this]() {
|
||||||
NetworkManager::instance().disconnect();
|
NetworkManager::instance().disconnectPermanent();
|
||||||
emit logoutRequested();
|
emit logoutRequested();
|
||||||
});
|
});
|
||||||
headerRow->addWidget(logoutBtn);
|
headerRow->addWidget(logoutBtn);
|
||||||
layout->addLayout(headerRow);
|
layout->addLayout(headerRow);
|
||||||
|
|
||||||
auto* titleIcon = new QLabel(card);
|
auto* titleIcon = new QLabel(card);
|
||||||
titleIcon->setPixmap(QPixmap(":/images/corpse").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
titleIcon->setPixmap(QPixmap(":/images/app_icon").scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
titleIcon->setAlignment(Qt::AlignCenter);
|
titleIcon->setAlignment(Qt::AlignCenter);
|
||||||
titleIcon->setStyleSheet("background:transparent;");
|
titleIcon->setStyleSheet("background:transparent;");
|
||||||
layout->addWidget(titleIcon);
|
layout->addWidget(titleIcon);
|
||||||
@@ -305,9 +310,7 @@ void LobbyWidget::onJoinRoom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LobbyWidget::onJoinTestRoom(int playerCount) {
|
void LobbyWidget::onJoinTestRoom(int playerCount) {
|
||||||
auto nick = _nicknameEdit->text().trimmed();
|
QJsonObject p;
|
||||||
if (nick.isEmpty()) nick = QStringLiteral("玩家");
|
p["player_count"] = playerCount;
|
||||||
QString roomId = QStringLiteral("TEST%1").arg(playerCount);
|
NetworkManager::instance().sendMessage("create_bot_room", p);
|
||||||
ElaMessageBar::information(ElaMessageBarType::Top, QStringLiteral("加入房间"), QStringLiteral("正在加入 ") + roomId + QStringLiteral(" ..."), 1500, this);
|
|
||||||
NetworkManager::instance().joinRoom(roomId, nick);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-2
@@ -1,6 +1,8 @@
|
|||||||
#include "LoginWidget.h"
|
#include "LoginWidget.h"
|
||||||
#include "SettingsManager.h"
|
#include "SettingsManager.h"
|
||||||
#include "NetworkManager.h"
|
#include "NetworkManager.h"
|
||||||
|
#include "SettingsDialog.h"
|
||||||
|
#include "UpdateChecker.h"
|
||||||
#include "ElaLineEdit.h"
|
#include "ElaLineEdit.h"
|
||||||
#include "ElaPushButton.h"
|
#include "ElaPushButton.h"
|
||||||
#include "ElaToggleSwitch.h"
|
#include "ElaToggleSwitch.h"
|
||||||
@@ -11,6 +13,8 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QLinearGradient>
|
#include <QLinearGradient>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
LoginWidget::LoginWidget(QWidget* parent) : QWidget(parent) {
|
LoginWidget::LoginWidget(QWidget* parent) : QWidget(parent) {
|
||||||
initUI();
|
initUI();
|
||||||
@@ -23,6 +27,12 @@ LoginWidget::LoginWidget(QWidget* parent) : QWidget(parent) {
|
|||||||
_connectBtn->setEnabled(true);
|
_connectBtn->setEnabled(true);
|
||||||
_loginBtn->setEnabled(true);
|
_loginBtn->setEnabled(true);
|
||||||
_registerBtn->setEnabled(true);
|
_registerBtn->setEnabled(true);
|
||||||
|
|
||||||
|
auto addr = _serverEdit->text().trimmed();
|
||||||
|
if (!addr.isEmpty()) {
|
||||||
|
SettingsManager::instance().set("network", "server_address", addr);
|
||||||
|
SettingsManager::instance().save();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
connect(&net, &NetworkManager::disconnected, this, [this]() {
|
connect(&net, &NetworkManager::disconnected, this, [this]() {
|
||||||
_statusLabel->setText(QStringLiteral("○ 未连接"));
|
_statusLabel->setText(QStringLiteral("○ 未连接"));
|
||||||
@@ -51,7 +61,20 @@ LoginWidget::LoginWidget(QWidget* parent) : QWidget(parent) {
|
|||||||
s.set("account", "bio", r["bio"].toString());
|
s.set("account", "bio", r["bio"].toString());
|
||||||
s.save();
|
s.save();
|
||||||
}
|
}
|
||||||
ElaMessageBar::success(ElaMessageBarType::TopRight, QStringLiteral("成功"), _nickname, 2000, this);
|
|
||||||
|
auto activeRoom = r["active_room"].toString();
|
||||||
|
if (!activeRoom.isEmpty()) {
|
||||||
|
auto ret = QMessageBox::question(this, QStringLiteral("检测到未完成的对局"),
|
||||||
|
QStringLiteral("你在房间 %1 中有一局正在进行的游戏。\n是否重新加入?").arg(activeRoom),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
if (ret == QMessageBox::Yes) {
|
||||||
|
QJsonObject p;
|
||||||
|
p["room_id"] = activeRoom;
|
||||||
|
p["player_id"] = r["active_player_id"].toString();
|
||||||
|
NetworkManager::instance().sendMessage("rejoin_room", p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElaMessageBar::success(ElaMessageBarType::TopRight, QStringLiteral("登录成功"), _nickname, 2000, this);
|
||||||
emit loggedIn(_nickname);
|
emit loggedIn(_nickname);
|
||||||
} else {
|
} else {
|
||||||
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("失败"), r["message"].toString(), 3000, this);
|
ElaMessageBar::error(ElaMessageBarType::TopRight, QStringLiteral("失败"), r["message"].toString(), 3000, this);
|
||||||
@@ -73,8 +96,13 @@ void LoginWidget::initUI() {
|
|||||||
lay->setContentsMargins(30, 20, 30, 20);
|
lay->setContentsMargins(30, 20, 30, 20);
|
||||||
lay->setSpacing(10);
|
lay->setSpacing(10);
|
||||||
|
|
||||||
|
auto* settingsBtn = new ElaPushButton(QStringLiteral("⚙"), card);
|
||||||
|
settingsBtn->setFixedSize(30, 26);
|
||||||
|
connect(settingsBtn, &ElaPushButton::clicked, this, [this]() { SettingsDialog dlg(this); dlg.exec(); });
|
||||||
|
lay->addWidget(settingsBtn, 0, Qt::AlignRight);
|
||||||
|
|
||||||
auto* icon = new QLabel(card);
|
auto* icon = new QLabel(card);
|
||||||
icon->setPixmap(QPixmap(":/images/corpse").scaled(56, 56, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
icon->setPixmap(QPixmap(":/images/app_icon").scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
icon->setAlignment(Qt::AlignCenter);
|
icon->setAlignment(Qt::AlignCenter);
|
||||||
icon->setStyleSheet("background:transparent;");
|
icon->setStyleSheet("background:transparent;");
|
||||||
lay->addWidget(icon);
|
lay->addWidget(icon);
|
||||||
@@ -114,8 +142,14 @@ void LoginWidget::initUI() {
|
|||||||
_passwordEdit->setPlaceholderText(QStringLiteral("密码"));
|
_passwordEdit->setPlaceholderText(QStringLiteral("密码"));
|
||||||
_passwordEdit->setEchoMode(QLineEdit::Password);
|
_passwordEdit->setEchoMode(QLineEdit::Password);
|
||||||
_passwordEdit->setFixedHeight(30);
|
_passwordEdit->setFixedHeight(30);
|
||||||
|
bool hasToken = !s.getString("account", "token").isEmpty() && s.getBool("account", "remember", false);
|
||||||
|
_passwordEdit->setPlaceholderText(hasToken ? QStringLiteral("留空则使用已保存的登录信息") : QStringLiteral("密码"));
|
||||||
lay->addWidget(_passwordEdit);
|
lay->addWidget(_passwordEdit);
|
||||||
|
|
||||||
|
_rememberHint = new QLabel(hasToken ? QStringLiteral("✓ 已记住登录信息") : "", card);
|
||||||
|
_rememberHint->setStyleSheet("color:#5cb85c;font-size:11px;background:transparent;");
|
||||||
|
lay->addWidget(_rememberHint);
|
||||||
|
|
||||||
auto* remRow = new QHBoxLayout();
|
auto* remRow = new QHBoxLayout();
|
||||||
auto* remLbl = new QLabel(QStringLiteral("记住登录"), card);
|
auto* remLbl = new QLabel(QStringLiteral("记住登录"), card);
|
||||||
remLbl->setStyleSheet("color:#e0e0e0;font-size:12px;background:transparent;");
|
remLbl->setStyleSheet("color:#e0e0e0;font-size:12px;background:transparent;");
|
||||||
@@ -168,17 +202,24 @@ void LoginWidget::onLogin() {
|
|||||||
QJsonObject p;
|
QJsonObject p;
|
||||||
|
|
||||||
if (!pass.isEmpty()) {
|
if (!pass.isEmpty()) {
|
||||||
|
if (user.isEmpty()) {
|
||||||
|
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请输入用户名"), 2000, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
p["username"] = user;
|
p["username"] = user;
|
||||||
p["password"] = pass;
|
p["password"] = pass;
|
||||||
|
ElaMessageBar::information(ElaMessageBarType::TopRight, QStringLiteral("登录"), QStringLiteral("正在使用密码验证..."), 1500, this);
|
||||||
} else {
|
} else {
|
||||||
auto token = SettingsManager::instance().getString("account", "token");
|
auto token = SettingsManager::instance().getString("account", "token");
|
||||||
if (!token.isEmpty()) {
|
if (!token.isEmpty()) {
|
||||||
p["token"] = token;
|
p["token"] = token;
|
||||||
|
ElaMessageBar::information(ElaMessageBarType::TopRight, QStringLiteral("登录"), QStringLiteral("正在使用已保存的登录信息..."), 1500, this);
|
||||||
} else {
|
} else {
|
||||||
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请输入密码"), 2000, this);
|
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请输入密码"), 2000, this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
p["version"] = QString(APP_VERSION);
|
||||||
NetworkManager::instance().sendMessage("login", p);
|
NetworkManager::instance().sendMessage("login", p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ private:
|
|||||||
ElaLineEdit* _usernameEdit = nullptr;
|
ElaLineEdit* _usernameEdit = nullptr;
|
||||||
ElaLineEdit* _passwordEdit = nullptr;
|
ElaLineEdit* _passwordEdit = nullptr;
|
||||||
ElaToggleSwitch* _rememberSwitch = nullptr;
|
ElaToggleSwitch* _rememberSwitch = nullptr;
|
||||||
|
QLabel* _rememberHint = nullptr;
|
||||||
ElaPushButton* _connectBtn = nullptr;
|
ElaPushButton* _connectBtn = nullptr;
|
||||||
ElaPushButton* _loginBtn = nullptr;
|
ElaPushButton* _loginBtn = nullptr;
|
||||||
ElaPushButton* _registerBtn = nullptr;
|
ElaPushButton* _registerBtn = nullptr;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "GameWidget.h"
|
#include "GameWidget.h"
|
||||||
#include "ProfileDialog.h"
|
#include "ProfileDialog.h"
|
||||||
#include "SettingsManager.h"
|
#include "SettingsManager.h"
|
||||||
|
#include "NetworkManager.h"
|
||||||
|
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -16,7 +17,7 @@ MainWindow::MainWindow(QWidget* parent) : QWidget(parent)
|
|||||||
|
|
||||||
void MainWindow::initUI() {
|
void MainWindow::initUI() {
|
||||||
setWindowTitle(QStringLiteral("冰冷的她醒来之前"));
|
setWindowTitle(QStringLiteral("冰冷的她醒来之前"));
|
||||||
setWindowIcon(QIcon(":/images/corpse"));
|
setWindowIcon(QIcon(":/images/app_icon"));
|
||||||
resize(1200, 800);
|
resize(1200, 800);
|
||||||
setMinimumSize(900, 600);
|
setMinimumSize(900, 600);
|
||||||
|
|
||||||
@@ -48,7 +49,13 @@ void MainWindow::initUI() {
|
|||||||
connect(_lobby, &LobbyWidget::joinedRoom, this, &MainWindow::switchToRoom);
|
connect(_lobby, &LobbyWidget::joinedRoom, this, &MainWindow::switchToRoom);
|
||||||
connect(_room, &RoomWidget::gameStarted, this, &MainWindow::switchToGame);
|
connect(_room, &RoomWidget::gameStarted, this, &MainWindow::switchToGame);
|
||||||
connect(_room, &RoomWidget::leftRoom, this, &MainWindow::switchToLobby);
|
connect(_room, &RoomWidget::leftRoom, this, &MainWindow::switchToLobby);
|
||||||
connect(_game, &GameWidget::gameFinished, this, &MainWindow::switchToLobby);
|
connect(_game, &GameWidget::gameFinished, this, [this]() {
|
||||||
|
NetworkManager::instance().leaveRoom();
|
||||||
|
switchToLobby();
|
||||||
|
});
|
||||||
|
connect(&NetworkManager::instance(), &NetworkManager::gameStarted, this, [this](const QJsonObject&) {
|
||||||
|
switchToGame();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::switchToLogin() { _stack->setCurrentWidget(_login); }
|
void MainWindow::switchToLogin() { _stack->setCurrentWidget(_login); }
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ void PlayerSeatWidget::updateSkillZone(const QJsonArray& cards) {
|
|||||||
card->setCardEnabled(false);
|
card->setCardEnabled(false);
|
||||||
_skillLayout->insertWidget(_skillLayout->count() - 1, card);
|
_skillLayout->insertWidget(_skillLayout->count() - 1, card);
|
||||||
_skillCards.append(card);
|
_skillCards.append(card);
|
||||||
|
connect(card, &CardWidget::rightClicked, this, &PlayerSeatWidget::skillCardZoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void playerClicked(const QString& playerId);
|
void playerClicked(const QString& playerId);
|
||||||
|
void skillCardZoom(const QString& typeId);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "ElaToggleSwitch.h"
|
#include "ElaToggleSwitch.h"
|
||||||
#include "ElaMessageBar.h"
|
#include "ElaMessageBar.h"
|
||||||
#include "NetworkManager.h"
|
#include "NetworkManager.h"
|
||||||
|
#include "SettingsDialog.h"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
@@ -85,6 +86,10 @@ void RoomWidget::initUI() {
|
|||||||
|
|
||||||
auto* btnRow = new QHBoxLayout();
|
auto* btnRow = new QHBoxLayout();
|
||||||
btnRow->setSpacing(10);
|
btnRow->setSpacing(10);
|
||||||
|
auto* settingsBtn = new ElaPushButton(QStringLiteral("⚙"), card);
|
||||||
|
settingsBtn->setFixedSize(30, 30);
|
||||||
|
connect(settingsBtn, &ElaPushButton::clicked, this, [this]() { SettingsDialog dlg(this); dlg.exec(); });
|
||||||
|
btnRow->addWidget(settingsBtn);
|
||||||
_leaveBtn = new ElaPushButton(QStringLiteral("离开房间"), card);
|
_leaveBtn = new ElaPushButton(QStringLiteral("离开房间"), card);
|
||||||
_leaveBtn->setFixedHeight(38);
|
_leaveBtn->setFixedHeight(38);
|
||||||
connect(_leaveBtn, &ElaPushButton::clicked, this, [this]() { NetworkManager::instance().leaveRoom(); });
|
connect(_leaveBtn, &ElaPushButton::clicked, this, [this]() { NetworkManager::instance().leaveRoom(); });
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
#include "ElaPushButton.h"
|
#include "ElaPushButton.h"
|
||||||
#include "ElaSlider.h"
|
#include "ElaSlider.h"
|
||||||
#include "ElaToggleSwitch.h"
|
#include "ElaToggleSwitch.h"
|
||||||
|
#include "ElaMessageBar.h"
|
||||||
|
#include "UpdateChecker.h"
|
||||||
|
#include "SettingsManager.h"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
@@ -28,7 +31,19 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent)
|
|||||||
{
|
{
|
||||||
setWindowTitle(QStringLiteral("设置"));
|
setWindowTitle(QStringLiteral("设置"));
|
||||||
setFixedSize(620, 460);
|
setFixedSize(620, 460);
|
||||||
setStyleSheet("QDialog{background:#1a1f2e;} QGroupBox{color:#8a8f9d;border:1px solid #2a3040;border-radius:8px;margin-top:10px;padding-top:16px;} QGroupBox::title{subcontrol-origin:margin;left:12px;padding:0 4px;}");
|
setStyleSheet(
|
||||||
|
"QDialog{background:#1a1f2e;color:#e0e0e0;}"
|
||||||
|
"QLabel{color:#e0e0e0;background:transparent;}"
|
||||||
|
"QGroupBox{color:#8a8f9d;border:1px solid #2a3040;border-radius:8px;margin-top:10px;padding-top:16px;}"
|
||||||
|
"QGroupBox::title{subcontrol-origin:margin;left:12px;padding:0 4px;}"
|
||||||
|
"QComboBox{background:#252b3d;color:#e0e0e0;border:1px solid #3a3f4e;border-radius:6px;padding:6px 10px;}"
|
||||||
|
"QComboBox::drop-down{border:none;}"
|
||||||
|
"QComboBox QAbstractItemView{background:#252b3d;color:#e0e0e0;border:1px solid #3a3f4e;selection-background-color:#5eb3e6;}"
|
||||||
|
"QRadioButton{color:#e0e0e0;spacing:6px;}"
|
||||||
|
"QRadioButton::indicator{width:14px;height:14px;border-radius:7px;border:2px solid #5eb3e6;background:transparent;}"
|
||||||
|
"QRadioButton::indicator:checked{background:#5eb3e6;}"
|
||||||
|
"QCheckBox{color:#e0e0e0;}"
|
||||||
|
);
|
||||||
|
|
||||||
auto* root = new QHBoxLayout(this);
|
auto* root = new QHBoxLayout(this);
|
||||||
root->setContentsMargins(0, 0, 0, 0);
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
@@ -42,6 +57,7 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent)
|
|||||||
_categoryList->addItem(QStringLiteral("语音"));
|
_categoryList->addItem(QStringLiteral("语音"));
|
||||||
_categoryList->addItem(QStringLiteral("音频"));
|
_categoryList->addItem(QStringLiteral("音频"));
|
||||||
_categoryList->addItem(QStringLiteral("显示"));
|
_categoryList->addItem(QStringLiteral("显示"));
|
||||||
|
_categoryList->addItem(QStringLiteral("关于"));
|
||||||
root->addWidget(_categoryList);
|
root->addWidget(_categoryList);
|
||||||
|
|
||||||
auto* rightPanel = new QWidget(this);
|
auto* rightPanel = new QWidget(this);
|
||||||
@@ -55,6 +71,7 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent)
|
|||||||
_pages->addWidget(buildVoicePage());
|
_pages->addWidget(buildVoicePage());
|
||||||
_pages->addWidget(buildAudioPage());
|
_pages->addWidget(buildAudioPage());
|
||||||
_pages->addWidget(buildDisplayPage());
|
_pages->addWidget(buildDisplayPage());
|
||||||
|
_pages->addWidget(buildAboutPage());
|
||||||
rLay->addWidget(_pages, 1);
|
rLay->addWidget(_pages, 1);
|
||||||
|
|
||||||
auto* btnRow = new QHBoxLayout();
|
auto* btnRow = new QHBoxLayout();
|
||||||
@@ -264,6 +281,68 @@ QWidget* SettingsDialog::buildDisplayPage() {
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QWidget* SettingsDialog::buildAboutPage() {
|
||||||
|
auto* page = new QWidget();
|
||||||
|
page->setStyleSheet(SS_PAGE);
|
||||||
|
auto* lay = new QVBoxLayout(page);
|
||||||
|
lay->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
auto* icon = new QLabel(page);
|
||||||
|
icon->setPixmap(QPixmap(":/images/app_icon").scaled(96, 96, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
|
icon->setAlignment(Qt::AlignCenter);
|
||||||
|
icon->setStyleSheet("background:transparent;");
|
||||||
|
lay->addWidget(icon);
|
||||||
|
|
||||||
|
auto* name = new QLabel(QStringLiteral("冰冷的她醒来之前"), page);
|
||||||
|
name->setStyleSheet("color:#5eb3e6;font-size:20px;font-weight:bold;background:transparent;");
|
||||||
|
name->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(name);
|
||||||
|
|
||||||
|
auto* ename = new QLabel(QStringLiteral("Embalming Girl - Digital Edition"), page);
|
||||||
|
ename->setStyleSheet("color:#8a8f9d;font-size:12px;background:transparent;");
|
||||||
|
ename->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(ename);
|
||||||
|
lay->addSpacing(10);
|
||||||
|
|
||||||
|
auto addInfo = [&](const QString& label, const QString& value) {
|
||||||
|
auto* row = new QHBoxLayout();
|
||||||
|
auto* l = new QLabel(label, page);
|
||||||
|
l->setStyleSheet(SS_DIM);
|
||||||
|
l->setFixedWidth(80);
|
||||||
|
auto* v = new QLabel(value, page);
|
||||||
|
v->setStyleSheet(SS_LABEL);
|
||||||
|
row->addWidget(l);
|
||||||
|
row->addWidget(v, 1);
|
||||||
|
lay->addLayout(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
addInfo(QStringLiteral("版本"), QString(APP_VERSION));
|
||||||
|
addInfo(QStringLiteral("游戏设计"), QStringLiteral("ゆお"));
|
||||||
|
addInfo(QStringLiteral("美术设计"), QStringLiteral("うすくち"));
|
||||||
|
addInfo(QStringLiteral("数字版开发"), QStringLiteral("Misaki"));
|
||||||
|
addInfo(QStringLiteral("框架"), QStringLiteral("Qt %1 + Go").arg(QT_VERSION_STR));
|
||||||
|
|
||||||
|
lay->addSpacing(12);
|
||||||
|
auto* updateBtn = new ElaPushButton(QStringLiteral("检查更新"), page);
|
||||||
|
updateBtn->setFixedSize(120, 34);
|
||||||
|
connect(updateBtn, &ElaPushButton::clicked, this, [this]() {
|
||||||
|
auto server = SettingsManager::instance().getString("network", "server_address");
|
||||||
|
if (!server.isEmpty())
|
||||||
|
UpdateChecker::instance().checkForUpdate(server, false);
|
||||||
|
else
|
||||||
|
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("提示"), QStringLiteral("请先设置服务器地址"), 2000, this);
|
||||||
|
});
|
||||||
|
lay->addWidget(updateBtn, 0, Qt::AlignCenter);
|
||||||
|
|
||||||
|
lay->addStretch();
|
||||||
|
auto* copy = new QLabel(QStringLiteral("© 2026 All Rights Reserved"), page);
|
||||||
|
copy->setStyleSheet("color:#3a3f4e;font-size:11px;background:transparent;");
|
||||||
|
copy->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(copy);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsDialog::loadFromConfig() {
|
void SettingsDialog::loadFromConfig() {
|
||||||
auto& s = SettingsManager::instance();
|
auto& s = SettingsManager::instance();
|
||||||
_nicknameEdit->setText(s.getString("general", "nickname", "Player"));
|
_nicknameEdit->setText(s.getString("general", "nickname", "Player"));
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ private:
|
|||||||
QWidget* buildVoicePage();
|
QWidget* buildVoicePage();
|
||||||
QWidget* buildAudioPage();
|
QWidget* buildAudioPage();
|
||||||
QWidget* buildDisplayPage();
|
QWidget* buildDisplayPage();
|
||||||
|
QWidget* buildAboutPage();
|
||||||
void loadFromConfig();
|
void loadFromConfig();
|
||||||
void saveToConfig();
|
void saveToConfig();
|
||||||
bool hasChanges() const;
|
bool hasChanges() const;
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
#include "UpdateChecker.h"
|
||||||
|
#include "ElaMessageBar.h"
|
||||||
|
#include "ElaPushButton.h"
|
||||||
|
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
|
UpdateChecker& UpdateChecker::instance() {
|
||||||
|
static UpdateChecker uc;
|
||||||
|
return uc;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateChecker::UpdateChecker(QObject* parent) : QObject(parent) {
|
||||||
|
_nam = new QNetworkAccessManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateChecker::checkForUpdate(const QString& serverBaseUrl, bool silent) {
|
||||||
|
_baseUrl = serverBaseUrl.endsWith("/") ? serverBaseUrl.chopped(1) : serverBaseUrl;
|
||||||
|
QString httpUrl = _baseUrl;
|
||||||
|
if (httpUrl.startsWith("ws://")) httpUrl.replace("ws://", "http://");
|
||||||
|
if (httpUrl.startsWith("wss://")) httpUrl.replace("wss://", "https://");
|
||||||
|
if (httpUrl.contains("/ws")) httpUrl = httpUrl.left(httpUrl.indexOf("/ws"));
|
||||||
|
_httpBase = httpUrl;
|
||||||
|
|
||||||
|
QString checkUrl = httpUrl + "/api/update/check?version=" + QString(APP_VERSION) + "&platform=win64";
|
||||||
|
auto* reply = _nam->get(QNetworkRequest(QUrl(checkUrl)));
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, silent]() {
|
||||||
|
onCheckFinished(reply, silent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateChecker::onCheckFinished(QNetworkReply* reply, bool silent) {
|
||||||
|
reply->deleteLater();
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
if (!silent)
|
||||||
|
ElaMessageBar::warning(ElaMessageBarType::TopRight, QStringLiteral("更新检查"),
|
||||||
|
QStringLiteral("无法连接更新服务器"), 3000, qApp->activeWindow());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto obj = QJsonDocument::fromJson(reply->readAll()).object();
|
||||||
|
if (!obj["update_available"].toBool()) {
|
||||||
|
if (!silent)
|
||||||
|
ElaMessageBar::success(ElaMessageBarType::TopRight, QStringLiteral("更新检查"),
|
||||||
|
QStringLiteral("当前已是最新版本 v%1").arg(APP_VERSION), 3000, qApp->activeWindow());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showUpdateDialog(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateChecker::showUpdateDialog(const QJsonObject& info) {
|
||||||
|
auto* dlg = new QDialog(qApp->activeWindow());
|
||||||
|
dlg->setWindowTitle(QStringLiteral("发现新版本"));
|
||||||
|
dlg->setFixedSize(460, 340);
|
||||||
|
dlg->setStyleSheet("QDialog{background:#1a1f2e;} QLabel{color:#e0e0e0;background:transparent;}");
|
||||||
|
|
||||||
|
auto* lay = new QVBoxLayout(dlg);
|
||||||
|
lay->setSpacing(10);
|
||||||
|
|
||||||
|
auto* title = new QLabel(QStringLiteral("🔄 有新版本可用"), dlg);
|
||||||
|
title->setStyleSheet("color:#5eb3e6;font-size:20px;font-weight:bold;");
|
||||||
|
title->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(title);
|
||||||
|
|
||||||
|
auto* verLabel = new QLabel(QStringLiteral("v%1 → v%2")
|
||||||
|
.arg(APP_VERSION, info["latest_version"].toString()), dlg);
|
||||||
|
verLabel->setStyleSheet("color:#d4c5a3;font-size:16px;");
|
||||||
|
verLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(verLabel);
|
||||||
|
|
||||||
|
qint64 sz = info["size"].toInteger();
|
||||||
|
auto* sizeLabel = new QLabel(QStringLiteral("大小: %1 MB").arg(sz / 1048576.0, 0, 'f', 1), dlg);
|
||||||
|
sizeLabel->setStyleSheet("color:#8a8f9d;font-size:12px;");
|
||||||
|
sizeLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(sizeLabel);
|
||||||
|
|
||||||
|
auto* notes = new QLabel(info["release_notes"].toString(), dlg);
|
||||||
|
notes->setStyleSheet("color:#e0e0e0;font-size:13px;background:rgba(0,0,0,0.3);border-radius:6px;padding:10px;");
|
||||||
|
notes->setWordWrap(true);
|
||||||
|
lay->addWidget(notes, 1);
|
||||||
|
|
||||||
|
bool mandatory = info["mandatory"].toBool();
|
||||||
|
if (mandatory) {
|
||||||
|
auto* w = new QLabel(QStringLiteral("⚠ 此更新为强制更新,无法跳过"), dlg);
|
||||||
|
w->setStyleSheet("color:#d94f5c;font-size:12px;font-weight:bold;");
|
||||||
|
w->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* btnRow = new QHBoxLayout();
|
||||||
|
btnRow->addStretch();
|
||||||
|
auto* skipBtn = new ElaPushButton(QStringLiteral("稍后"), dlg);
|
||||||
|
skipBtn->setFixedSize(80, 34);
|
||||||
|
if (mandatory) skipBtn->setEnabled(false);
|
||||||
|
connect(skipBtn, &ElaPushButton::clicked, dlg, &QDialog::reject);
|
||||||
|
|
||||||
|
auto* updateBtn = new ElaPushButton(QStringLiteral("立即更新"), dlg);
|
||||||
|
updateBtn->setFixedSize(120, 34);
|
||||||
|
|
||||||
|
QString downloadPath = info["download_url"].toString();
|
||||||
|
connect(updateBtn, &ElaPushButton::clicked, dlg, [this, dlg, downloadPath]() {
|
||||||
|
dlg->accept();
|
||||||
|
downloadAndInstall(downloadPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnRow->addWidget(skipBtn);
|
||||||
|
btnRow->addWidget(updateBtn);
|
||||||
|
lay->addLayout(btnRow);
|
||||||
|
|
||||||
|
dlg->exec();
|
||||||
|
dlg->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateChecker::downloadAndInstall(const QString& urlPath) {
|
||||||
|
QString tmpDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/embalming_update";
|
||||||
|
QDir().mkpath(tmpDir);
|
||||||
|
QString zipPath = tmpDir + "/update.zip";
|
||||||
|
|
||||||
|
auto* dlg = new QDialog(qApp->activeWindow());
|
||||||
|
dlg->setWindowTitle(QStringLiteral("正在更新"));
|
||||||
|
dlg->setFixedSize(420, 150);
|
||||||
|
dlg->setStyleSheet("QDialog{background:#1a1f2e;} QLabel{color:#e0e0e0;background:transparent;}");
|
||||||
|
auto* lay = new QVBoxLayout(dlg);
|
||||||
|
auto* status = new QLabel(QStringLiteral("正在下载更新包..."), dlg);
|
||||||
|
status->setStyleSheet("font-size:14px;");
|
||||||
|
lay->addWidget(status);
|
||||||
|
auto* bar = new QProgressBar(dlg);
|
||||||
|
bar->setRange(0, 100);
|
||||||
|
bar->setStyleSheet("QProgressBar{border:1px solid #3a3f4e;border-radius:4px;background:#252b3d;height:20px;} QProgressBar::chunk{background:#5eb3e6;border-radius:3px;}");
|
||||||
|
lay->addWidget(bar);
|
||||||
|
auto* detail = new QLabel(dlg);
|
||||||
|
detail->setStyleSheet("color:#8a8f9d;font-size:11px;");
|
||||||
|
lay->addWidget(detail);
|
||||||
|
dlg->show();
|
||||||
|
|
||||||
|
QString fullUrl = _httpBase + urlPath;
|
||||||
|
auto* reply = _nam->get(QNetworkRequest(QUrl(fullUrl)));
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::downloadProgress, dlg, [bar, detail](qint64 recv, qint64 total) {
|
||||||
|
if (total > 0) bar->setValue(int(recv * 100 / total));
|
||||||
|
detail->setText(QStringLiteral("%1 / %2 MB").arg(recv / 1048576.0, 0, 'f', 1).arg(total / 1048576.0, 0, 'f', 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, dlg, [this, reply, dlg, status, bar, zipPath]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
status->setText(QStringLiteral("下载失败: ") + reply->errorString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status->setText(QStringLiteral("正在保存..."));
|
||||||
|
bar->setValue(100);
|
||||||
|
|
||||||
|
QFile f(zipPath);
|
||||||
|
if (!f.open(QIODevice::WriteOnly)) {
|
||||||
|
status->setText(QStringLiteral("保存失败"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.write(reply->readAll());
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
status->setText(QStringLiteral("正在准备更新,程序即将重启..."));
|
||||||
|
|
||||||
|
QString updaterPath = QCoreApplication::applicationDirPath() + "/updater.exe";
|
||||||
|
if (!QFile::exists(updaterPath)) {
|
||||||
|
status->setText(QStringLiteral("错误: 找不到 updater.exe"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString appDir = QCoreApplication::applicationDirPath();
|
||||||
|
qint64 pid = QCoreApplication::applicationPid();
|
||||||
|
|
||||||
|
QStringList args;
|
||||||
|
args << "--pid" << QString::number(pid)
|
||||||
|
<< "--zip" << zipPath
|
||||||
|
<< "--target" << appDir
|
||||||
|
<< "--launch" << "Embalming_Girl.exe";
|
||||||
|
|
||||||
|
QProcess::startDetached(updaterPath, args, appDir);
|
||||||
|
|
||||||
|
QTimer::singleShot(500, qApp, &QApplication::quit);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef UPDATECHECKER_H
|
||||||
|
#define UPDATECHECKER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
#define APP_VERSION "1.0.2"
|
||||||
|
|
||||||
|
class UpdateChecker : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
static UpdateChecker& instance();
|
||||||
|
void checkForUpdate(const QString& serverBaseUrl, bool silent = false);
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit UpdateChecker(QObject* parent = nullptr);
|
||||||
|
void onCheckFinished(QNetworkReply* reply, bool silent);
|
||||||
|
void showUpdateDialog(const QJsonObject& info);
|
||||||
|
void downloadAndInstall(const QString& urlPath);
|
||||||
|
|
||||||
|
QNetworkAccessManager* _nam = nullptr;
|
||||||
|
QString _baseUrl;
|
||||||
|
QString _httpBase;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -143,12 +143,28 @@ void VoiceManager::processRemoteAudio(const QByteArray& frame) {
|
|||||||
if (frame.size() < 2 + idLen) return;
|
if (frame.size() < 2 + idLen) return;
|
||||||
QString senderId = QString::fromUtf8(frame.mid(2, idLen));
|
QString senderId = QString::fromUtf8(frame.mid(2, idLen));
|
||||||
QByteArray pcm = frame.mid(2 + idLen);
|
QByteArray pcm = frame.mid(2 + idLen);
|
||||||
|
if (pcm.isEmpty()) return;
|
||||||
|
|
||||||
if (!_remotes.contains(senderId)) {
|
if (!_remotes.contains(senderId)) {
|
||||||
|
auto outDev = _outputDev.isNull() ? QMediaDevices::defaultAudioOutput() : _outputDev;
|
||||||
|
if (outDev.isNull()) {
|
||||||
|
qWarning() << "[Voice] No audio output device";
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto fmt = audioFormat();
|
auto fmt = audioFormat();
|
||||||
auto* sink = new QAudioSink(_outputDev, fmt, this);
|
if (!outDev.isFormatSupported(fmt)) {
|
||||||
auto* buf = new AudioRingBuffer(FRAME_BYTES * 50, this);
|
qWarning() << "[Voice] Format not supported by output device";
|
||||||
|
}
|
||||||
|
auto* sink = new QAudioSink(outDev, fmt, this);
|
||||||
|
auto* buf = new AudioRingBuffer(FRAME_BYTES * 80, this);
|
||||||
|
sink->setBufferSize(FRAME_BYTES * 20);
|
||||||
sink->start(buf);
|
sink->start(buf);
|
||||||
|
if (sink->error() != QAudio::NoError) {
|
||||||
|
qWarning() << "[Voice] Failed to start audio sink:" << sink->error();
|
||||||
|
delete sink; delete buf;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qDebug() << "[Voice] Created playback for" << senderId;
|
||||||
_remotes[senderId] = {sink, buf, 0};
|
_remotes[senderId] = {sink, buf, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user