This commit is contained in:
2026-06-14 15:54:35 +08:00
commit 3caeb07e54
2804 changed files with 373231 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
#pragma once
/**
* @brief 菜单
* @author Misaki
*
* 基于Ela UI的菜单控件
*/
#include <ElaMenu.h>
#include <QMenu>
#include <QAction>
#include <QPoint>
#include <QScopedPointer> // 智能指针
#include "Setting.h"
#include "networkmanager.h"
#include "socketmanager.h"
class Menu final : public ElaMenu
{
Q_OBJECT
public:
explicit Menu(QWidget *parent = nullptr);
~Menu() override;
void showMenu(const QPoint &pos);
signals:
void closeMainWindow(); // 自定义关闭主窗口的信号
void startPlay(); // 自定义开始播放的信号
private:
void createMenu();
QAction *toggleThe; /// 切换主题(全局)
QAction *startSingleExchangeAction; /// 开启单次对话
QAction *startContinueExchangeAction; /// 开启连续对话
QAction *settingsAction; /// 设置
QAction *closeAction; /// 关闭
QScopedPointer<Setting> settingWindow; // 使用智能指针管理 Setting 窗口
private slots:
void toggleTheme();
// void startExchange();
};
+166
View File
@@ -0,0 +1,166 @@
#include "menu.h"
#include "ElaTheme.h"
#include "LAppLive2DManager.hpp"
#include <QDebug>
#include <QTimer>
#include "TextRenderer.h"
#include "AppCore.h"
Menu::Menu(QWidget *parent)
: ElaMenu(parent)
{
// 设置默认主题
eTheme->setThemeMode(ElaThemeType::Dark);
createMenu();
}
Menu::~Menu() {
AppCore::destroy(); // 显式销毁 AppCore
}
void Menu::createMenu()
{
toggleThe = addAction("切换主题");
// 单次对话功能按钮
startSingleExchangeAction = addAction("单次对话(测试)");
// 连续对话功能按钮
startContinueExchangeAction = addAction("连续对话(测试)");
// 添加设置按钮
settingsAction = addAction("设置");
// 添加关闭按钮
closeAction = addAction("关闭");
// 连接信号与槽
// 切换主题按钮
connect(toggleThe, &QAction::triggered, this, [this]() {
toggleTheme();
});
// 单次对话功能
connect(startSingleExchangeAction, &QAction::triggered, this, []() {
qDebug() << "Start SingleExchange triggered";
AppCore::getInstance()->SingleExchange();
});
// 连续对话功能 TODO: 待开发
connect(startContinueExchangeAction, &QAction::triggered, this, []() {
qDebug() << "Start ContinueExchange triggered";
});
// 设置按钮
connect(settingsAction, &QAction::triggered, this, [this]() {
qDebug() << "Settings triggered";
AppCore::getInstance()->tryToInit();
// 打开设置窗口
// 如果 Setting 窗口已经存在,则不再创建
if (settingWindow) {
settingWindow->show();
return;
}
// 动态创建 Setting 窗口
settingWindow.reset(new Setting()); // 使用智能指针管理
// 显示 Setting 窗口
settingWindow->show();
});
// 关闭
connect(closeAction, &QAction::triggered, this, [this]() {
emit closeMainWindow(); // 发射关闭信号
});
}
void Menu::showMenu(const QPoint &pos)
{
// 在指定位置显示菜单
exec(pos);
}
void Menu::toggleTheme()
{
if (eTheme->getThemeMode() == ElaThemeType::Light) {
eTheme->setThemeMode(ElaThemeType::Dark);
} else {
eTheme->setThemeMode(ElaThemeType::Light);
}
}
/*
void Menu::startExchange()
{
// 列出所有音频输入设备
QList<QString> devices = AudioInput::getAvailableAudioInputDevices();
qDebug() << "可用录音设备:";
for (const QString &device : devices) {
qDebug() << device;
}
// 设置当前录音设备(假设选择第一个设备)
if (!devices.isEmpty()) {
qDebug() << "选择的录音设备是: " << devices.first();
AudioInput::getInstance()->setAudioInputDevice(devices.first());
qDebug() << "当前录音设备: " << AudioInput::getInstance()->audioInput(); // 检查当前录音设备
}
// 第一次需要主动录音来启动 信号与槽状态机(FSM)
qDebug() << "开始录音";
AudioInput::getInstance()->startAutoStopAudio();
// 检查录音状态
if (AudioInput::getInstance()->state() == QMediaRecorder::RecordingState) {
qDebug() << "录音已启动";
} else {
qDebug() << "录音启动失败";
}
// 连接信号和槽
// 播放回答
// 当完整接受wav文件后播放相关的wav文件
connect(SocketManager::getInstance(), &SocketManager::revWavFileFinish, [this](const QString &filePath, const QString &response, const float duration) {
LAppLive2DManager::GetInstance()->StartLipSync(filePath.toUtf8().constData());
AudioOutput::getInstance()->setAudioPath(filePath);
AudioOutput::getInstance()->playAudio();
TextRenderer::getInstance()->addText(response, 40.0f, QColor("#FF69B4"), duration);
});
// 开始录音
// 当播放完成后继续开始录音
connect(AudioOutput::getInstance(), &AudioOutput::playbackFinished, [this]() {
qDebug() << "开始录音";
AudioInput::getInstance()->startAutoStopAudio();
// 检查录音状态
if (AudioInput::getInstance()->state() == QMediaRecorder::RecordingState) {
qDebug() << "录音已启动";
} else {
qDebug() << "录音启动失败";
}
});
// 上传录音
// 当录音完成时,发送wav文件
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte, [this](const QByteArray &wavData) {
qDebug() << "录音完成,开始上传录音文件...";
if (wavData.isEmpty()) {
qWarning() << "录音数据为空!";
return;
}
qDebug() << "准备发送WAV数据,大小:" << wavData.size() << "字节";
SocketManager::getInstance()->sendWavFile(wavData);
});
}
*/
+1
View File
@@ -0,0 +1 @@
本目录下为右键菜单的相关代码
@@ -0,0 +1,99 @@
//
// Created by Administrator on 2025/2/16.
//
#pragma once
#include <QOpenGLWidget>
#include <QVector>
#include <QString>
#include <QColor>
#include <QElapsedTimer>
#include <QVector2D>
#include <QFontMetrics>
#include <QLinearGradient>
class TextRenderer {
public:
// 删除拷贝构造函数和赋值运算符
TextRenderer(const TextRenderer&) = delete;
void operator=(const TextRenderer&) = delete;
// 获取单例实例
static TextRenderer* getInstance()
{
if(instance == nullptr){
instance = new TextRenderer();
}
return instance;
}
struct TextInstance {
QString text; // 文本内容
QVector2D basePosition; // 基础位置(Y轴)
QColor primaryColor; // 主要文字颜色
QColor outlineColor; // 轮廓颜色
float duration; // 显示总时长(秒)
qint64 startTime; // 开始显示时间(毫秒)
bool isDropping; // 是否正在下坠
qint64 dropStartTime; // 下坠开始时间
float dropYVelocity; // Y轴下落速度
float alpha; // 透明度
QList<QPoint> charPositions; // 字符位置
QList<int> charWidths; // 每个字符宽度
int visibleChars; // 可见字符数
bool flowCompleted; // 流式显示是否完成
float holdDuration; // 实际使用的停留时间
qint64 flowEndTime; // 流式完成时间戳
TextInstance() : isDropping(false), dropYVelocity(0.0f),
alpha(1.0f), visibleChars(0),
flowCompleted(false), holdDuration(0.5f),
flowEndTime(0) {}
};
void setWindowSize(int w, int h);
void addText(const QString &text, float yPos,
const QColor &color, float duration);
void update();
void render();
void setGlobalFont(const QFont &newFont);
/**
* 参数建议值:<br>
效果类型 gravity dampFactor holdDuration <br>
柔和下落 600.0f 0.85f 1.0f <br>
快速坠落 1200.0f 0.6f 0.3f <br>
弹性效果 900.0f 0.75f 0.8f <br>
真实物理模拟 980.0f 0.82f 0.5f
*/
void setHoldDuration(const float seconds) { defaultHoldDuration = seconds; }
void setGravity(const float g) { gravity = g; }
void setDampFactor(const float damp) { dampFactor = damp; }
// 释放单例
static void releaseInstance() {
if (instance) {
delete instance;
instance = nullptr;
}
}
private:
explicit TextRenderer(); // 构造函数私有化
void updateFlowPositions(TextInstance &instance);
void updateDropPositions(TextInstance &instance, float deltaTime);
private:
static TextRenderer *instance;
QList<TextInstance> activeTexts; /// 当前显示的文字
QElapsedTimer globalTimer; /// 全局计时器
QFont font; /// 全局字体
int windowWidth; /// 窗口宽度
int windowHeight; /// 窗口高度
qint64 lastFrameTime; /// 上一帧的时间
// 一些自定义参数
float defaultHoldDuration; /// 默认停留时间(秒)
float gravity; /// 重力加速度(像素/秒²)
float dampFactor; /// 碰撞阻尼系数
};
@@ -0,0 +1,248 @@
//
// Created by Administrator on 2025/2/16.
//
/**
* 用于渲染文本显示
*/
#include "TextRenderer.h"
#include <QPainter>
#include <QOpenGLPaintDevice>
#include <cmath>
#include <QRandomGenerator>
TextRenderer *TextRenderer::instance = nullptr;
TextRenderer::TextRenderer() : windowWidth(800), // 默认窗口宽
windowHeight(600), // 默认窗口高
lastFrameTime(0),
defaultHoldDuration(0.5f), // 默认停留0.5秒
gravity(980.0f), // 默认重力
dampFactor(0.82f) // 默认阻尼
{
globalTimer.start();
font.setFamily("Microsoft YaHei");
font.setPixelSize(28);
font.setWeight(QFont::Bold);
}
void TextRenderer::setGlobalFont(const QFont &newFont)
{
font = newFont;
font.setWeight(QFont::Bold);
}
void TextRenderer::setWindowSize(int w, int h)
{
windowWidth = w;
windowHeight = h;
}
void TextRenderer::addText(const QString &text, float yPos,
const QColor &color, float duration)
{
TextInstance instance;
instance.text = text;
instance.basePosition = QVector2D(0, yPos);
instance.primaryColor = color;
instance.outlineColor = QColor(0, 0, 0, 180);
instance.duration = duration;
instance.startTime = globalTimer.elapsed();
instance.holdDuration = defaultHoldDuration; // 应用当前全局设置
QFontMetrics metrics(font);
instance.charWidths.clear();
for (const QChar &ch : text) {
instance.charWidths.append(metrics.horizontalAdvance(ch));
}
// 初始位置计算
updateFlowPositions(instance);
activeTexts.append(instance);
}
void TextRenderer::updateFlowPositions(TextInstance &instance)
{
QFontMetrics metrics(font);
const int rightMargin = 20; // 右侧留白
// 计算可见部分总宽度
int visibleWidth = 0;
for (int i = 0; i < instance.visibleChars; ++i) {
visibleWidth += instance.charWidths[i];
}
// 动态计算起始位置
int startX = qMin(
windowWidth - visibleWidth - rightMargin, // 优先保证右侧空间
(windowWidth - visibleWidth) / 2 // 次选居中显示
);
// 边界保护:至少保留20px左侧边距
startX = qMax(20, startX);
// 更新字符位置
int currentX = startX;
instance.charPositions.clear();
for (int i = 0; i < instance.text.size(); ++i) {
if (i < instance.visibleChars) {
instance.charPositions.append(QPoint(currentX, instance.basePosition.y()));
currentX += instance.charWidths[i];
} else {
instance.charPositions.append(QPoint(-10000, -10000));
}
}
// 自动滚动调整:当文字溢出时整体左移
if (currentX > windowWidth - rightMargin) {
int overflow = currentX - (windowWidth - rightMargin);
for (QPoint &pos : instance.charPositions) {
if (pos.x() != -10000) {
pos.rx() -= overflow;
}
}
}
}
void TextRenderer::updateDropPositions(TextInstance &instance, float deltaTime)
{
const float floorY = windowHeight - 30;
instance.dropYVelocity += gravity * deltaTime; // 使用全局重力值
float deltaY = instance.dropYVelocity * deltaTime;
bool hasCollision = false;
for (QPoint &pos : instance.charPositions) {
float newY = pos.y() + deltaY;
if (newY >= floorY) {
newY = floorY;
instance.dropYVelocity = -qAbs(instance.dropYVelocity) * dampFactor; // 使用全局阻尼系数
hasCollision = true;
pos.rx() += QRandomGenerator::global()->bounded(-3, 4);
}
pos.setY(newY);
}
// 碰撞后处理
if (hasCollision) {
// 速度衰减到临界值时开始加速透明
if (qAbs(instance.dropYVelocity) < 100.0f) {
instance.alpha *= 0.92f; // 加快透明度衰减速度
}
// 完全静止后强制移除
if (qAbs(instance.dropYVelocity) < 5.0f) {
instance.alpha = 0.0f;
}
}
// 常规透明度衰减
instance.alpha = qMax(0.0f, instance.alpha * 0.98f);
// 添加随机水平扰动(只在有速度时)
if (qAbs(instance.dropYVelocity) > 10.0f) {
for (QPoint &pos : instance.charPositions) {
pos.rx() += QRandomGenerator::global()->bounded(-1, 2);
}
}
}
void TextRenderer::update()
{
qint64 currentTime = globalTimer.elapsed();
float deltaTime = (currentTime - lastFrameTime) / 1000.0f;
lastFrameTime = currentTime;
auto it = activeTexts.begin();
while (it != activeTexts.end()) {
TextInstance &instance = *it;
if (instance.isDropping) {
updateDropPositions(instance, deltaTime); // 使用统一的deltaTime
// 强化消失条件:Y轴速度接近零 或 透明度低于阈值
if ((qAbs(instance.dropYVelocity) < 5.0f && instance.alpha < 0.3f)
|| instance.alpha < 0.01f) {
it = activeTexts.erase(it);
continue;
}
} else {
// 流式显示更新
float progress = (currentTime - instance.startTime) / 1000.0f / instance.duration;
progress = qMin(progress, 1.0f);
if (!instance.flowCompleted) {
int newVisible = qMin(instance.text.size(),
static_cast<int>(progress * instance.text.size()));
if (newVisible != instance.visibleChars) {
instance.visibleChars = newVisible;
updateFlowPositions(instance);
}
// 流式显示完成检测
if (progress >= 1.0f) {
instance.flowCompleted = true;
instance.flowEndTime = currentTime; // 记录完成时间
}
}
// 已流式完成但未开始下坠
else if (!instance.isDropping) {
// 检查停留时间是否结束
if (currentTime - instance.flowEndTime >= instance.holdDuration * 1000) {
instance.isDropping = true;
instance.dropStartTime = currentTime;
// 最终居中定位
int totalWidth = 0;
for (const int w : instance.charWidths) totalWidth += w;
const int startX = (windowWidth - totalWidth) / 2;
int currentX = startX;
instance.charPositions.clear();
for (int i = 0; i < instance.text.size(); ++i) {
instance.charPositions.append(QPoint(currentX, instance.basePosition.y()));
currentX += instance.charWidths[i];
}
}
}
}
++it;
}
}
void TextRenderer::render()
{
QOpenGLPaintDevice device(windowWidth, windowHeight);
QPainter painter(&device);
painter.setFont(font);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
for (const auto &instance : activeTexts) {
QColor mainColor = instance.primaryColor;
mainColor.setAlphaF(instance.alpha);
QColor outlineColor = instance.outlineColor;
outlineColor.setAlphaF(instance.alpha * 0.7f);
for (int i = 0; i < instance.charPositions.size(); ++i) {
const QPoint &pos = instance.charPositions[i];
if (pos.x() < -9999) continue; // 跳过隐藏字符
// 绘制轮廓
painter.setPen(outlineColor);
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
if (dx == 0 && dy == 0) continue;
painter.drawText(pos + QPoint(dx, dy), QString(instance.text[i]));
}
}
// 绘制主体
painter.setPen(mainColor);
painter.drawText(pos, QString(instance.text[i]));
}
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// Created by Administrator on 2025/3/4.
//
#ifndef AIRI_DESKTOPGRIL_AUDIOPAGE_H
#define AIRI_DESKTOPGRIL_AUDIOPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
class ElaComboBox;
class ElaSpinBox;
class ElaProgressBar;
class ElaPushButton;
class AudioPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit AudioPage(QWidget* parent = nullptr);
~AudioPage() override;
private:
ElaComboBox* audioInputDeviceComboBox = nullptr;
ElaSpinBox* audioInputSpinBox = nullptr;
ElaProgressBar* audioInputProgressBar = nullptr;
ElaPushButton* audioAutoThresholdStartButton = nullptr;
ElaPushButton* audioManualThresholdStartButton = nullptr;
ElaPushButton* testAudioPlayButton = nullptr;
};
#endif //AIRI_DESKTOPGRIL_AUDIOPAGE_H
+21
View File
@@ -0,0 +1,21 @@
//
// Created by Administrator on 2025/2/27.
//
#ifndef AIRI_DESKTOPGRIL_BASEPAGE_H
#define AIRI_DESKTOPGRIL_BASEPAGE_H
#include <ElaScrollPage.h>
class QVBoxLayout;
class BasePage : public ElaScrollPage
{
Q_OBJECT
public:
Q_INVOKABLE explicit BasePage(QWidget* parent = nullptr);
~BasePage();
protected:
void createCustomWidget(QString desText);
};
#endif //AIRI_DESKTOPGRIL_BASEPAGE_H
+23
View File
@@ -0,0 +1,23 @@
//
// Created by Administrator on 2025/2/28.
//
#ifndef AIRI_DESKTOPGRIL_HOMEPAGE_H
#define AIRI_DESKTOPGRIL_HOMEPAGE_H
#include "BasePage.h"
class ElaMenu;
class HomePage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit HomePage(QWidget* parent = nullptr);
~HomePage();
Q_SIGNALS:
Q_SIGNAL void audioNavigation();
Q_SIGNAL void modelShopNavigation();
};
#endif //AIRI_DESKTOPGRIL_HOMEPAGE_H
+41
View File
@@ -0,0 +1,41 @@
//
// Created by Administrator on 2025/4/1.
//
/**
* @brief 模型页面
* 暂时只做最简单功能切换模型
*/
#pragma once
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaComboBox.h"
#include "ElaSlider.h"
#include <QUrl>
#include <utility>
class ModelPage final : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit ModelPage(QWidget* parent = nullptr);
std::pair<QString, QString> splitPath(const QString& fullPath);
~ModelPage() override;
private:
// 设置当前模型
ElaLineEdit* modelUrlEdit = nullptr; /// 模型Url 编辑框
ElaPushButton* modelChoosePushButton = nullptr; /// 选择模型按钮
ElaPushButton* modelUsePushButton = nullptr; /// 使用模型按钮
ElaSlider* modelSlider = nullptr; /// 滑块(用于设置模型实时大小)
QUrl modelFileUrl;
QString modelFilePathFirst;
QString modelFilePathSecond;
};
+39
View File
@@ -0,0 +1,39 @@
//
// Created by Administrator on 2025/3/2.
//
#pragma once
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
class ElaPushButton;
class ElaLineEdit;
class NetWorkPage final : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit NetWorkPage(QWidget* parent = nullptr);
~NetWorkPage() override;
private:
void initUI();
void initWebSocketClient();
private:
// websocket 控件
ElaPushButton* websocketPushButton = nullptr;
ElaLineEdit* websocketLineEdit = nullptr;
// 连接测试
ElaPushButton* connectTestPushButton = nullptr;
// 连接
ElaPushButton* connectPushButton = nullptr;
// 断开
ElaPushButton* disconnectPushButton = nullptr;
// 发送测试按钮
ElaPushButton* sendTestPushButton = nullptr;
};
+30
View File
@@ -0,0 +1,30 @@
//
// Created by Administrator on 2025/3/30.
//
#ifndef YOSUGA_RENDERPAGE_H
#define YOSUGA_RENDERPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaComboBox.h"
class RenderPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit RenderPage(QWidget* parent = nullptr);
~RenderPage();
private:
// 帧率设置
ElaComboBox* frameRateComboBox = nullptr;
};
#endif //YOSUGA_RENDERPAGE_H
+65
View File
@@ -0,0 +1,65 @@
//
// Created by Administrator on 2025/1/21.
//
#pragma once
#include <ElaWidget.h>
#include <ElaWindow.h>
#include <ElaPushButton.h>
#include <ElaScrollPage.h>
#include <QStackedWidget>
#include "HomePage.h"
#include "NetworkPage.h"
#include "UISetting.h"
#include "AudioPage.h"
#include "RenderPage.h"
#include "ModelPage.h"
class Setting : public ElaWindow
{
Q_OBJECT
public:
explicit Setting(QWidget *parent = nullptr);
~Setting();
private:
/**
* 初始化所有页面指针
* @author : Misaki
*/
void initPages();
/**
* 初始化导航栏
* @author : Misaki
*/
void initNavigationBar();
/**
* 初始化上下文切换
* @author : Misaki
*/
void initContent();
private slots:
void toggleTheme();
private:
// 页面指针
HomePage *homePage;
NetWorkPage *networkPage;
UISetting *uiSetting;
AudioPage *audioPage;
RenderPage *renderPage;
ModelPage *modelPage;
// 节点键值
QString basePageKey;
//
ElaPushButton *themeToggleButton;
};
+26
View File
@@ -0,0 +1,26 @@
//
// Created by Administrator on 2025/3/2.
//
#pragma once
#include "BasePage.h"
class ElaRadioButton;
class ElaToggleSwitch;
class ElaComboBox;
class UISetting : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit UISetting(QWidget* parent = nullptr);
~UISetting();
private:
ElaComboBox* _themeComboBox = nullptr;
ElaToggleSwitch* _micaSwitchButton = nullptr;
ElaToggleSwitch* _logSwitchButton = nullptr;
ElaRadioButton* _minimumButton = nullptr;
ElaRadioButton* _compactButton = nullptr;
ElaRadioButton* _maximumButton = nullptr;
ElaRadioButton* _autoButton = nullptr;
};
+132
View File
@@ -0,0 +1,132 @@
//
// Created by Administrator on 2025/3/4.
//
#include "AudioPage.h"
#include <QHBoxLayout>
#include "ElaComboBox.h"
#include "ElaPlainTextEdit.h"
#include "ElaProgressBar.h"
#include "ElaScrollPageArea.h"
#include "ElaSlider.h"
#include "ElaSpinBox.h"
#include "ElaText.h"
#include "ElaMessageBar.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "TextRenderer.h"
#include "LAppLive2DManager.hpp"
AudioPage::AudioPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("AudioPage");
audioInputDeviceComboBox = new ElaComboBox(this);
audioInputDeviceComboBox->setToolTip("选择可用的录音设备");
QStringList comboList = AudioInput::getAvailableAudioInputDevices();
audioInputDeviceComboBox->addItems(comboList);
ElaScrollPageArea* comboBoxArea = new ElaScrollPageArea(this);
QHBoxLayout* comboBoxLayout = new QHBoxLayout(comboBoxArea);
ElaText* comboBoxText = new ElaText("录音设备", this);
comboBoxText->setTextPixelSize(15);
comboBoxLayout->addWidget(comboBoxText);
comboBoxLayout->addWidget(audioInputDeviceComboBox);
comboBoxLayout->addStretch();
comboBoxLayout->addSpacing(10);
connect(audioInputDeviceComboBox, &ElaComboBox::currentTextChanged, [this](const QString& text) {
AudioInput::getInstance()->setAudioInputDevice(text);
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "成功设置 " + text + " 为当前录音设备", 800.0, this);
});
audioInputSpinBox = new ElaSpinBox(this); // SpinBox
audioInputSpinBox->setRange(0, 10000);
audioInputProgressBar = new ElaProgressBar(this);
audioInputProgressBar->setRange(0, 10000);
// 关闭ProgressBar的百分比显示
audioInputProgressBar->setTextVisible(false);
// 将SpinBox和ProgressBar的数值相互绑定
connect(audioInputSpinBox, QOverload<int>::of(&ElaSpinBox::valueChanged), [this](int value) {
audioInputProgressBar->setValue(value);
});
connect(audioInputProgressBar, QOverload<int>::of(&ElaProgressBar::valueChanged), [this](int value) {
audioInputSpinBox->setValue(value);
});
// 绑定实时录音阈值到ProgressBar,同时归一到0~1000范围内
connect(AudioInput::getInstance(), &AudioInput::rmsRealValue, [this](const qreal value) {
audioInputProgressBar->setValue(value);
});
// 当计算完成最优阈值
connect(AudioInput::getInstance(), &AudioInput::thresholdCalculated, [this](qreal value) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "自动计算出的最优阈值为:" + QString::number(value), 1000, this);
// AudioInput会自动设置计算出的最优阈值
});
audioAutoThresholdStartButton = new ElaPushButton("自动最优阈值", this);
audioAutoThresholdStartButton->setToolTip("点击后保持当前环境音5秒,自动计算出最合适的静音检测阈值");
connect(audioAutoThresholdStartButton, &ElaPushButton::clicked, [=]() {
AudioInput::getInstance()->startAutoThresholdClu(5000);
qDebug("开始计算最优阈值");
});
audioManualThresholdStartButton = new ElaPushButton("手动设置阈值", this);
audioManualThresholdStartButton->setToolTip("如果你觉得自动计算的不准的话");
connect(audioManualThresholdStartButton, &ElaPushButton::clicked, [this]() {
AudioInput::getInstance()->setSilenceThreshold(audioInputSpinBox->value());
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "手动设置的阈值为:" + QString::number(audioInputSpinBox->value()), 1000, this);
});
ElaScrollPageArea* audioInputProgressBarArea = new ElaScrollPageArea(this);
QHBoxLayout* audioInputProgressBarLayout = new QHBoxLayout(audioInputProgressBarArea);
ElaText* audioInputProgressBarText = new ElaText("静音检测阈值", this);
audioInputProgressBarText->setToolTip("测试当前环境的静音阈值,用于对话中的静音检测");
audioInputProgressBarText->setTextPixelSize(15);
audioInputProgressBarLayout->addWidget(audioInputProgressBarText);
audioInputProgressBarLayout->addWidget(audioInputProgressBar, 1);
audioInputProgressBarLayout->addWidget(audioInputSpinBox);
audioInputProgressBarLayout->addStretch(); // 添加弹性空间将后续控件推到右侧
audioInputProgressBarLayout->addWidget(audioAutoThresholdStartButton);
audioInputProgressBarLayout->addWidget(audioManualThresholdStartButton);
audioInputProgressBarLayout->addStretch();
audioInputProgressBarLayout->addSpacing(10);
testAudioPlayButton = new ElaPushButton("播放测试", this);
testAudioPlayButton->setToolTip("播放一段测试音频来检测播放功能是否正常,注意观察模型嘴唇以及文字下落动画");
ElaScrollPageArea* testAudioArea = new ElaScrollPageArea(this);
QHBoxLayout* testAudioLayout = new QHBoxLayout(testAudioArea);
ElaText* testAudioText = new ElaText("测试", this);
testAudioText->setTextPixelSize(15);
testAudioLayout->addWidget(testAudioText);
testAudioLayout->addStretch(); // 添加弹性空间将后续控件推到右侧
testAudioLayout->addWidget(testAudioPlayButton);
testAudioLayout->addSpacing(10);
connect(testAudioPlayButton, &ElaPushButton::clicked, [this]() {
const QString text = "あれアイリーじゃないよ!急にいなくなるからどこに行ったのかと思えば~";
constexpr float duration = 6.0f; // 音频时长
TextRenderer::getInstance()->addText(text, 40.0f, QColor("#FF69B4"), duration);
LAppLive2DManager::GetInstance()->StartLipSync("Resources/TestFiles/test.wav");
AudioOutput::getInstance()->playUrl(QUrl("Resources/TestFiles/test.wav"));
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("音频设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(comboBoxArea);
centerLayout->addWidget(audioInputProgressBarArea);
centerLayout->addWidget(testAudioArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
AudioPage::~AudioPage()
{
}
+89
View File
@@ -0,0 +1,89 @@
//
// Created by Administrator on 2025/2/27.
//
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "BasePage.h"
#include "ElaMenu.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaToolButton.h"
BasePage::BasePage(QWidget* parent)
: ElaScrollPage(parent)
{
connect(eTheme, &ElaTheme::themeModeChanged, this, [=, this]() {
if (!parent)
{
update();
}
});
}
BasePage::~BasePage()
{
}
void BasePage::createCustomWidget(QString desText)
{
// 顶部元素
QWidget* customWidget = new QWidget(this);
ElaText* subTitleText = new ElaText(this);
subTitleText->setText("https://github.com/Liniyous/ElaWidgetTools");
subTitleText->setTextInteractionFlags(Qt::TextSelectableByMouse);
subTitleText->setTextPixelSize(11);
ElaToolButton* documentationButton = new ElaToolButton(this);
documentationButton->setFixedHeight(35);
documentationButton->setIsTransparent(false);
documentationButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
//_toolButton->setPopupMode(QToolButton::MenuButtonPopup);
documentationButton->setText("Documentation");
documentationButton->setElaIcon(ElaIconType::FileDoc);
ElaMenu* documentationMenu = new ElaMenu(this);
documentationMenu->addElaIconAction(ElaIconType::CardsBlank, "CardsBlank");
documentationMenu->addElaIconAction(ElaIconType::EarthAmericas, "EarthAmericas");
documentationButton->setMenu(documentationMenu);
ElaToolButton* sourceButton = new ElaToolButton(this);
sourceButton->setFixedHeight(35);
sourceButton->setIsTransparent(false);
sourceButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
sourceButton->setText("Source");
sourceButton->setElaIcon(ElaIconType::NfcSymbol);
ElaMenu* sourceMenu = new ElaMenu(this);
sourceMenu->addElaIconAction(ElaIconType::FireBurner, "FireBurner");
sourceMenu->addElaIconAction(ElaIconType::Galaxy, "Galaxy~~~~");
sourceButton->setMenu(sourceMenu);
ElaToolButton* themeButton = new ElaToolButton(this);
themeButton->setFixedSize(35, 35);
themeButton->setIsTransparent(false);
themeButton->setElaIcon(ElaIconType::MoonStars);
connect(themeButton, &ElaToolButton::clicked, this, [=]() {
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark : ElaThemeType::Light);
});
QHBoxLayout* buttonLayout = new QHBoxLayout();
buttonLayout->addWidget(documentationButton);
buttonLayout->addSpacing(5);
buttonLayout->addWidget(sourceButton);
buttonLayout->addStretch();
buttonLayout->addWidget(themeButton);
buttonLayout->addSpacing(15);
ElaText* descText = new ElaText(this);
descText->setText(desText);
descText->setTextPixelSize(13);
QVBoxLayout* topLayout = new QVBoxLayout(customWidget);
topLayout->setContentsMargins(0, 0, 0, 0);
topLayout->addWidget(subTitleText);
topLayout->addSpacing(5);
topLayout->addLayout(buttonLayout);
topLayout->addSpacing(5);
topLayout->addWidget(descText);
setCustomWidget(customWidget);
}
+154
View File
@@ -0,0 +1,154 @@
//
// Created by Administrator on 2025/2/28.
//
#include "HomePage.h"
#include <QDebug>
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QVBoxLayout>
#include "ElaAcrylicUrlCard.h"
#include "ElaFlowLayout.h"
#include "ElaImageCard.h"
#include "ElaMenu.h"
#include "ElaMessageBar.h"
#include "ElaNavigationRouter.h"
#include "ElaPopularCard.h"
#include "ElaScrollArea.h"
#include "ElaText.h"
#include "ElaToolTip.h"
HomePage::HomePage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("Home");
setTitleVisible(false);
setContentsMargins(2, 2, 0, 0);
// 标题卡片区域
ElaText* desText = new ElaText("UI By ElaWidgetTools", this);
desText->setTextPixelSize(18);
ElaText* titleText = new ElaText("Yosuga!", this);
titleText->setTextPixelSize(35);
QVBoxLayout* titleLayout = new QVBoxLayout();
titleLayout->setContentsMargins(30, 60, 0, 0);
titleLayout->addWidget(desText);
titleLayout->addWidget(titleText);
ElaImageCard* backgroundCard = new ElaImageCard(this);
backgroundCard->setBorderRadius(10);
backgroundCard->setFixedHeight(400);
backgroundCard->setMaximumAspectRatio(1.7);
backgroundCard->setCardImage(QImage("Resources/Pic/Airi/Airi_Background.png"));
ElaAcrylicUrlCard* urlCard1 = new ElaAcrylicUrlCard(this);
urlCard1->setCardPixmapSize(QSize(62, 62));
urlCard1->setFixedSize(195, 225);
urlCard1->setTitlePixelSize(17);
urlCard1->setTitleSpacing(25);
urlCard1->setSubTitleSpacing(13);
urlCard1->setUrl("https://github.com/Misakityan/Yosuga");
urlCard1->setCardPixmap(QPixmap("Resources/Pic/Others/img.png"));
urlCard1->setTitle("Yosuga Github");
urlCard1->setSubTitle("Star++!");
ElaToolTip* urlCard1ToolTip = new ElaToolTip(urlCard1);
urlCard1ToolTip->setToolTip("点击前往本项目GitHub");
ElaAcrylicUrlCard* urlCard2 = new ElaAcrylicUrlCard(this);
urlCard2->setCardPixmapSize(QSize(62, 62));
urlCard2->setFixedSize(195, 225);
urlCard2->setTitlePixelSize(17);
urlCard2->setTitleSpacing(25);
urlCard2->setSubTitleSpacing(13);
urlCard2->setUrl("https://space.bilibili.com/140315806");
urlCard2->setCardPixmap(QPixmap("Resources/Pic/Others/Misaki.jpg"));
urlCard2->setTitle("Misaki");
urlCard2->setSubTitle("1841738040@qq.com");
ElaToolTip* urlCard2ToolTip = new ElaToolTip(urlCard2);
urlCard2ToolTip->setToolTip("点击前往 Misaki 的个人主页");
ElaScrollArea* cardScrollArea = new ElaScrollArea(this);
cardScrollArea->setWidgetResizable(true);
cardScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
cardScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
cardScrollArea->setIsGrabGesture(true, 0);
cardScrollArea->setIsOverShoot(Qt::Horizontal, true);
QWidget* cardScrollAreaWidget = new QWidget(this);
cardScrollAreaWidget->setStyleSheet("background-color:transparent;");
cardScrollArea->setWidget(cardScrollAreaWidget);
QHBoxLayout* urlCardLayout = new QHBoxLayout();
urlCardLayout->setSpacing(15);
urlCardLayout->setContentsMargins(30, 0, 0, 6);
urlCardLayout->addWidget(urlCard1);
urlCardLayout->addWidget(urlCard2);
urlCardLayout->addStretch();
QVBoxLayout* cardScrollAreaWidgetLayout = new QVBoxLayout(cardScrollAreaWidget);
cardScrollAreaWidgetLayout->setContentsMargins(0, 0, 0, 0);
cardScrollAreaWidgetLayout->addStretch();
cardScrollAreaWidgetLayout->addLayout(urlCardLayout);
QVBoxLayout* backgroundLayout = new QVBoxLayout(backgroundCard);
backgroundLayout->setContentsMargins(0, 0, 0, 0);
backgroundLayout->addLayout(titleLayout);
backgroundLayout->addWidget(cardScrollArea);
// 推荐卡片
ElaText* flowText = new ElaText("快速转到", this);
flowText->setTextPixelSize(20);
QHBoxLayout* flowTextLayout = new QHBoxLayout();
flowTextLayout->setContentsMargins(33, 0, 0, 0);
flowTextLayout->addWidget(flowText);
// ElaFlowLayout
// 模型商店卡片
ElaPopularCard* ModeShopCard = new ElaPopularCard(this);
connect(ModeShopCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() {
Q_EMIT modelShopNavigation();
});
ModeShopCard->setCardPixmap(QPixmap("Resources/Pic/Others/Live2D.png"));
ModeShopCard->setTitle("模型设置");
ModeShopCard->setSubTitle("属于你的Live2D模型");
ModeShopCard->setInteractiveTips("By Misaki");
ModeShopCard->setDetailedText("选择你喜欢的Live2D模型");
// 音频设置卡片
ElaPopularCard* AudioSettingCard = new ElaPopularCard(this);
connect(AudioSettingCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() {
Q_EMIT audioNavigation();
});
AudioSettingCard->setTitle("音频设置");
AudioSettingCard->setSubTitle("录音与播放的设置");
AudioSettingCard->setCardPixmap(QPixmap("Resources/Pic/control/AutomationProperties.png"));
AudioSettingCard->setInteractiveTips("By Misaki");
AudioSettingCard->setDetailedText("自定义音频与播放的相关设定,打造最舒适的交流环境。");
ElaFlowLayout* flowLayout = new ElaFlowLayout(0, 5, 5);
flowLayout->setContentsMargins(30, 0, 0, 0);
flowLayout->setIsAnimation(true);
flowLayout->addWidget(ModeShopCard);
flowLayout->addWidget(AudioSettingCard);
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("Home");
QVBoxLayout* centerVLayout = new QVBoxLayout(centralWidget);
centerVLayout->setSpacing(0);
centerVLayout->setContentsMargins(0, 0, 0, 0);
centerVLayout->addWidget(backgroundCard);
centerVLayout->addSpacing(20);
centerVLayout->addLayout(flowTextLayout);
centerVLayout->addSpacing(10);
centerVLayout->addLayout(flowLayout);
centerVLayout->addStretch();
addCentralWidget(centralWidget);
}
HomePage::~HomePage()
{
}
+191
View File
@@ -0,0 +1,191 @@
//
// Created by Administrator on 2025/4/1.
//
#include "ModelPage.h"
#include <QFileDialog>
#include <QHBoxLayout>
#include "ElaMessageBar.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include <QtConcurrent>
#include <QOpenGLContext>
#include <QOffscreenSurface>
#include <QFutureWatcher>
#include "LAppLive2DManager.hpp"
ModelPage::ModelPage(QWidget *parent)
: BasePage(parent) {
// 预览窗口标题
setWindowTitle("ModelPage");
modelUrlEdit = new ElaLineEdit(this); // 模型Url Edit
modelUrlEdit->setFixedWidth(300);
modelUrlEdit->setPlaceholderText("用于显示当前的模型Url");
modelChoosePushButton = new ElaPushButton("选择模型", this);
modelChoosePushButton->setToolTip("选择.model3.json结尾的文件");
modelUsePushButton = new ElaPushButton("使用模型", this);
modelUsePushButton->setToolTip("使用选择的模型或Url对应的模型");
ElaScrollPageArea *modelSetArea = new ElaScrollPageArea(this);
QHBoxLayout *modelSetLayout = new QHBoxLayout(modelSetArea);
ElaText *modelSetText = new ElaText("模型设置", this);
modelSetText->setTextPixelSize(15);
modelSetLayout->addWidget(modelSetText);
modelSetLayout->addWidget(modelUrlEdit);
modelSetLayout->addStretch();
modelSetLayout->addWidget(modelChoosePushButton);
modelSetLayout->addWidget(modelUsePushButton);
modelSetLayout->addSpacing(10);
// 创建滑块控件和标签
ElaText *modelSizeText = new ElaText("模型大小比例设置", this);
modelSizeText->setToolTip("实时调整模型大小");
modelSizeText->setTextPixelSize(15);
modelSlider = new ElaSlider(this); // 滑块(用于设置模型实时大小)
modelSlider->setRange(0, 99); // 设置范围
modelSlider->setValue(85); // 设置默认值
modelSlider->setOrientation(Qt::Horizontal); // 水平方向
// 创建独立的区域容器
ElaScrollPageArea *modelSliderArea = new ElaScrollPageArea(this);
QHBoxLayout *modelSliderLayout = new QHBoxLayout(modelSliderArea);
// 添加到布局(加一个标签显示数值)
ElaText *modelSliderValueText = new ElaText("85%", this);
modelSliderValueText->setTextPixelSize(14);
connect(modelSlider, &ElaSlider::valueChanged, this, [modelSliderValueText](const int value){
modelSliderValueText->setText(QString("%1%").arg(value + 1));
LAppLive2DManager::GetInstance()->ModelSizeChange(100 - value); // 实时更新模型大小
});
modelSliderLayout->addWidget(modelSizeText);
modelSliderLayout->addWidget(modelSlider);
modelSliderLayout->addWidget(modelSliderValueText);
modelSliderLayout->addStretch(); // 让内容靠左
connect(modelChoosePushButton, &ElaPushButton::clicked, this, [this]() {
// 创建对话框对象(使用 heap 分配,由 Qt 对象树管理内存)
auto *fileDialog = new QFileDialog(this);
fileDialog->setWindowTitle("选择模型文件");
fileDialog->setNameFilter("Live2D Model (*.model3.json)");
// 设置初始目录
const QString exeDir = QCoreApplication::applicationDirPath(); // 获取当前exe所在目录的本地路径
fileDialog->setDirectory(exeDir); // 设置初始目录为当前 exe 所在目录
// 连接信号:当用户选中文件并点击打开时
connect(fileDialog, &QFileDialog::fileSelected, this, [this, fileDialog](const QString &file) {
modelFileUrl = QUrl::fromLocalFile(file);
if (!modelFileUrl.isEmpty()) {
const QString t = modelFileUrl.toLocalFile();
const std::pair<QString, QString> path = this->splitPath(t);
this->modelFilePathFirst = path.first;
this->modelFilePathSecond = path.second;
this->modelUrlEdit->setText(t);
ElaMessageBar::success(ElaMessageBarType::BottomRight, "模型设置", "模型选择成功", 2000, this);
}
// 用完即弃,自动清理内存
fileDialog->deleteLater();
});
// 处理取消的情况(防止内存泄漏)
connect(fileDialog, &QFileDialog::rejected, fileDialog, &QObject::deleteLater);
// 显示对话框(非阻塞,不会卡住主界面)
fileDialog->open();
});
connect(modelUsePushButton, &ElaPushButton::clicked, this, [this]() {
// 模型使用
if (modelFileUrl.isEmpty()) {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "模型设置", "似乎并没有选择模型", 800.0, this);
return;
}
// UI 状态设置为加载中
modelUsePushButton->setEnabled(false); // 禁用使用按钮
modelUsePushButton->setText("加载中"); // 修改按钮文本
modelChoosePushButton->setEnabled(false); // 禁用选择按钮
// 获取路径字符串 (必须按值传递给lambda)
std::string dir = this->modelFilePathFirst.toStdString();
std::string filename = this->modelFilePathSecond.toStdString();
// 启动异步任务
QFuture<LAppModel *> future = QtConcurrent::run([dir, filename]() -> LAppModel * {
// 以下代码在子线程执行
// 创建临时 OpenGL 上下文
auto *context = new QOpenGLContext();
// 关键点:设置与全局共享上下文共享 (这样主线程才能看到纹理)
context->setShareContext(QOpenGLContext::globalShareContext());
if (!context->create()) {
delete context;
return nullptr;
}
// 创建离屏表面 (因为子线程没有窗口,需要一个假的绘制表面)
auto *surface = new QOffscreenSurface();
surface->setFormat(context->format());
surface->create();
// 绑定上下文
if (!context->makeCurrent(surface)) {
delete surface;
delete context;
return nullptr;
}
// 执行真正的耗时加载
// 调用在 Manager 里新写的函数
LAppModel *model = LAppLive2DManager::GetInstance()->LoadModelInstance(dir, filename);
// 清理子线程资源
context->doneCurrent();
delete surface;
delete context;
return model;
});
// 监控任务结束
auto *watcher = new QFutureWatcher<LAppModel *>();
connect(watcher, &QFutureWatcher<LAppModel *>::finished, this, [this, watcher]() {
// 下面的代码在主线程中执行
LAppModel *newModel = watcher->result();
if (newModel) {
// 调用挂载函数,瞬间完成切换
LAppLive2DManager::GetInstance()->MountLoadedModel(newModel);
ElaMessageBar::success(ElaMessageBarType::BottomRight, "成功", "模型加载完成", 2000, this);
} else {
ElaMessageBar::error(ElaMessageBarType::BottomRight, "错误", "模型加载失败 (OpenGL环境异常)", 2000, this);
}
// 恢复 UI
modelUsePushButton->setEnabled(true);
modelUsePushButton->setText("使用模型");
modelChoosePushButton->setEnabled(true);
watcher->deleteLater();
});
// 开始监控
watcher->setFuture(future);
});
QWidget *centralWidget = new QWidget(this);
centralWidget->setWindowTitle("模型设置");
QVBoxLayout *centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(modelSetArea);
centerLayout->addWidget(modelSliderArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
// 返回 pair<目录路径, 文件名>
std::pair<QString, QString> ModelPage::splitPath(const QString &fullPath) {
const QFileInfo fileInfo(fullPath);
// 获取目录部分(自动处理末尾斜杠)
QString dirPath = fileInfo.dir().absolutePath() + "/";
// 获取文件名部分(如果是目录则返回空)
QString fileName = fileInfo.fileName();
return {dirPath, fileName};
}
ModelPage::~ModelPage() {
}
+186
View File
@@ -0,0 +1,186 @@
//
// Created by Administrator on 2025/3/2.
//
#include "NetworkPage.h"
#include <QHBoxLayout>
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "ElaMessageBar.h"
#include "websocketmanager.h"
#include "NetWorkDO.h"
#include <QFile>
NetWorkPage::NetWorkPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("NetworkPage");
this->initUI(); // 初始化UI
this->initWebSocketClient(); // 初始化websocket客户端(主要是相关的信号与槽)
}
NetWorkPage::~NetWorkPage()
{
}
void NetWorkPage::initUI() {
// websocket UI
websocketPushButton = new ElaPushButton("设定",this);
websocketPushButton->setToolTip("设定服务端WebSocket地址");
websocketLineEdit = new ElaLineEdit(this);
websocketLineEdit->setPlaceholderText("请输入服务端WebSocket地址");
websocketLineEdit->setFixedWidth(300); // 设置websocketLineEdit框的宽度
ElaScrollPageArea* websocketToggleSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* websocketToggleSwitchLayout = new QHBoxLayout(websocketToggleSwitchArea);
ElaText* websocketToggleSwitchText = new ElaText("服务端WebSocket地址:", this);
websocketToggleSwitchText->setTextPixelSize(15);
websocketToggleSwitchLayout->addWidget(websocketToggleSwitchText);
websocketToggleSwitchLayout->addWidget(websocketLineEdit);
websocketToggleSwitchLayout->addStretch();
websocketToggleSwitchLayout->addWidget(websocketPushButton);
websocketToggleSwitchLayout->addSpacing(10);
// 连通测试按钮
connectTestPushButton = new ElaPushButton("连通测试",this);
connectTestPushButton->setToolTip("测试与服务器连通性(如果成功连通会自动连上服务器)");
connectPushButton = new ElaPushButton("连接",this);
disconnectPushButton = new ElaPushButton("断开",this);
sendTestPushButton = new ElaPushButton("发送测试",this);
ElaScrollPageArea* connectTestArea = new ElaScrollPageArea(this); // 创建一个滚动页面
QHBoxLayout* connectTestLayout = new QHBoxLayout(connectTestArea);
connectTestLayout->addWidget(connectTestPushButton); // 将连通测试按钮添加到布局中
connectTestLayout->addStretch(); // 添加一个空格
connectTestLayout->addWidget(sendTestPushButton); // 将发送测试按钮添加到布局中
connectTestLayout->addWidget(disconnectPushButton); // 将断开按钮添加到布局中
connectTestLayout->addWidget(connectPushButton); // 将连接按钮添加到布局中
connectTestLayout->addSpacing(10);
// 添加到布局
QWidget* centralWidget = new QWidget(this); // 中心部件
centralWidget->setWindowTitle("连接设置"); // 中心部件标题
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget); // 中心部件布局
centerLayout->addWidget(websocketToggleSwitchArea); // 将websocketToggleSwitchArea添加到布局中
centerLayout->addWidget(connectTestArea); // 将connectTestArea添加到布局中
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0); // 设置布局的边距
addCentralWidget(centralWidget, true, true, 0); // 添加中心部件
}
void NetWorkPage::initWebSocketClient() {
auto* client = WebSocketClient::getInstance(); // 获取单例实例(设置一个默认地址)
auto* netDO = NetworkDO::getInstance();
// 注入:将底层发送能力赋予 NetworkDO
netDO->registerSender([client](const QString& type, const QJsonObject& data){
client->sendJson(type, data);
});
// 监听:底层收到数据 -> NetworkDO 解析
connect(client, &WebSocketClient::jsonReceived,
netDO, &NetworkDO::onDataReceived);
// 连接成功的处理
connect(client, &WebSocketClient::connected, this, [this]() {
ElaMessageBar::success(ElaMessageBarType::TopRight, "WebSocket", "连接成功", 800.0, this);
});
// 连接失败的处理
connect(client, &WebSocketClient::error, this, [this](const QString& errorMsg) {
ElaMessageBar::error(ElaMessageBarType::TopLeft, "WebSocket错误", errorMsg, 1500.0, this);
});
// 断开连接的处理
connect(client, &WebSocketClient::disconnected, this, [this]() {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "WebSocket", "连接已断开", 800.0, this);
});
// 接收数据处理
connect(client, &WebSocketClient::jsonReceived, this, [](const QString &type, const QJsonObject &data) {
qDebug() << "Received JSON data: " << type << " " << data;
});
connect(websocketPushButton, &ElaPushButton::clicked, this, [this, client]() { // 设置服务端websocket地址
const QUrl url(websocketLineEdit->text().trimmed()); // 从LineEdit中获取服务端websocket地址
// 初始化客户端
if (client->setConfiguration(url)) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置",
QString("服务器地址已设置为: %1").arg(url.toString()), 800.0, this);
return;
}
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连接设置",
QString("服务器地址存在问题"), 800.0, this);
});
connect(connectTestPushButton, &ElaPushButton::clicked, this, [this, client]() {
if (client->isConnected()) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试",
"当前已连通", 800.0, this);
return;
}
client->connectToServer(); // 连接
// 使用定时器延迟检查连接状态
QTimer::singleShot(1000, this, [this, client]() {
if (!client->isConnected()) {
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连通测试",
"无法连接到服务器,请检查地址和服务器状态", 1500.0, this);
return;
}
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试",
"连通测试成功", 800.0, this);
});
});
connect(connectPushButton, &ElaPushButton::clicked, this, [this, client]() {
if (client->isConnected()) {
ElaMessageBar::information(ElaMessageBarType::TopRight, "连接状态",
"已连接,无需重复连接", 800.0, this);
return;
}
const QString urlStr = websocketLineEdit->text().trimmed();
if (urlStr.isEmpty()) {
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连接",
"请先设置服务器地址", 800.0, this);
return;
}
// 确保使用正确的地址
client->setConfiguration(QUrl(urlStr));
client->connectToServer();
// 连接结果会在 connected/error 信号中处理
ElaMessageBar::information(ElaMessageBarType::TopRight, "连接",
"正在连接服务器...", 800.0, this);
});
connect(disconnectPushButton, &ElaPushButton::clicked, this, [this, client]() {
if (!client->isConnected()) {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接",
"当前未连接", 800.0, this);
return;
}
client->disconnectFromServer();
ElaMessageBar::success(ElaMessageBarType::TopRight, "断开连接",
"已断开连接", 800.0, this);
});
connect(sendTestPushButton, &ElaPushButton::clicked, this, [this, netDO, client]() {
if (!client->isConnected()) {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接",
"当前未连接", 800.0, this);
return;
}
// 创建数据包
QFile test_wav_file("Resources/TestFiles/test.wav");
if (!test_wav_file.open(QIODevice::ReadOnly)) {
qDebug() << "Failed to open test.wav";
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "发送测试",
"无法打开测试音频文件", 1500.0, this);
return;
}
QByteArray wavData = test_wav_file.readAll();
QString base64Str = QString::fromLatin1(wavData.toBase64());
AudioDataTransferObject packet;
packet.setData("Owner", "client")
.setData("isStream", true)
.setData("sequence", 42)
.setData("text", "Hello World")
.setData("data", base64Str); // 填入测试音频数据
netDO->sendPacket(packet);
ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试",
"已成功发送数据包", 1000.0, this);
});
}
+54
View File
@@ -0,0 +1,54 @@
//
// Created by Administrator on 2025/3/30.
//
#include "RenderPage.h"
#include <QHBoxLayout>
#include <QtWidgets>
#include "ElaComboBox.h"
#include "ElaMessageBar.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "GLCore.h"
#include "AppContext.h"
RenderPage::RenderPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("RenderPage");
frameRateComboBox = new ElaComboBox(this);
QStringList frameRateComboList = GLCore::getFrameRateList();
frameRateComboBox->addItems(frameRateComboList);
ElaScrollPageArea* frameRateComboBoxArea = new ElaScrollPageArea(this);
QHBoxLayout* frameRateComboBoxLayout = new QHBoxLayout(frameRateComboBoxArea);
ElaText* frameRateComboBoxText = new ElaText("帧率设置", this);
frameRateComboBoxText->setTextPixelSize(15);
frameRateComboBoxLayout->addWidget(frameRateComboBoxText);
frameRateComboBoxLayout->addStretch();
frameRateComboBoxLayout->addWidget(frameRateComboBox);
frameRateComboBoxLayout->addSpacing(10);
connect(frameRateComboBox, &ElaComboBox::currentTextChanged, this, [this](const QString& text) {
AppContext::GetGLCore()->setFrameRate(GLCore::getFrameRateMap().value(text));
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("渲染设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(frameRateComboBoxArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
RenderPage::~RenderPage()
{
}
+114
View File
@@ -0,0 +1,114 @@
//
// Created by Administrator on 2025/1/21.
//
#include <ElaTheme.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSvgRenderer>
#include <QPainter>
#include "Setting.h"
#include "socketmanager.h"
Setting::Setting(QWidget *parent)
: ElaWindow(parent)
{
// 设置窗口标题
setWindowTitle("设置");
// 设置窗口图标
setWindowIcon(QIcon("Resources/Pic/Airi/Airi_s.svg"));
// 初始化窗口
resize(1000, 740);
// 禁用窗口缩放按钮
setWindowButtonFlag(ElaAppBarType::MaximizeButtonHint, false); // 隐藏最大化按钮
setWindowButtonFlag(ElaAppBarType::MinimizeButtonHint, false); // 隐藏最小化按钮
// 移动窗口到屏幕中心
this->moveToCenter();
this->setWindowFlag(Qt::Tool); // 隐藏应用程序图标
this->setWindowFlag(Qt::WindowStaysOnTopHint); // 默认设置窗口始终在顶部
// 设置用户信息卡
this->setUserInfoCardTitle("Yosuga");
this->setUserInfoCardSubTitle("联系维度的桥梁!");
// 加载 SVG 图片
QSvgRenderer renderer(QString("Resources/Pic/Airi/Airi.svg"));
// 创建 QPixmap 并绘制 SVG
QPixmap pixmap(64, 64);
pixmap.fill(Qt::transparent); // 设置透明背景
QPainter painter(&pixmap);
renderer.render(&painter);
this->setUserInfoCardPixmap(pixmap);
// 初始化页面
initPages();
// 创建导航栏和内容区域
initNavigationBar();
// 创建上下文
initContent();
// 设置初始主题
eTheme->setThemeMode(ElaThemeType::Dark);
}
Setting::~Setting()
{
}
void Setting::initPages()
{
homePage = new HomePage(this);
networkPage = new NetWorkPage(this);
uiSetting = new UISetting(this);
audioPage = new AudioPage(this);
renderPage = new RenderPage(this);
modelPage = new ModelPage(this);
}
void Setting::initNavigationBar()
{
// 添加主页节点(顶级节点)
addPageNode("主页", homePage, ElaIconType::House);
// 添加模型商店节点
addPageNode("模型设置", modelPage, ElaIconType::Shop);
// 添加网络连接设置节点
addPageNode("连接设置", networkPage, ElaIconType::NetworkWired);
// 添加音频设置节点
addPageNode("音频设置", audioPage, ElaIconType::MusicNote);
// 添加渲染设置节点
addPageNode("渲染设置", renderPage, ElaIconType::ArrowsRotate);
QString uiSettingKey;
addFooterNode("UI设置", uiSetting, uiSettingKey, 0, ElaIconType::GearComplex);
}
void Setting::initContent()
{
connect(homePage, &HomePage::modelShopNavigation, this, [&](){
this->navigation(modelPage->property("ElaPageKey").toString());
});
connect(homePage, &HomePage::audioNavigation, this, [&](){
this->navigation(audioPage->property("ElaPageKey").toString());
});
}
void Setting::toggleTheme()
{
if (eTheme->getThemeMode() == ElaThemeType::Light) {
eTheme->setThemeMode(ElaThemeType::Dark);
} else {
eTheme->setThemeMode(ElaThemeType::Light);
}
}
+166
View File
@@ -0,0 +1,166 @@
//
// Created by Administrator on 2025/3/2.
//
#include "UISetting.h"
#include <QDebug>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "ElaApplication.h"
#include "ElaComboBox.h"
#include "ElaLog.h"
#include "ElaRadioButton.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaToggleSwitch.h"
#include "ElaWindow.h"
UISetting::UISetting(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
ElaWindow* window = dynamic_cast<ElaWindow*>(parent);
setWindowTitle("Setting");
ElaText* themeText = new ElaText("主题设置", this);
themeText->setWordWrap(false);
themeText->setTextPixelSize(18);
_themeComboBox = new ElaComboBox(this);
_themeComboBox->addItem("日间模式");
_themeComboBox->addItem("夜间模式");
ElaScrollPageArea* themeSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* themeSwitchLayout = new QHBoxLayout(themeSwitchArea);
ElaText* themeSwitchText = new ElaText("主题切换", this);
themeSwitchText->setWordWrap(false);
themeSwitchText->setTextPixelSize(15);
themeSwitchLayout->addWidget(themeSwitchText);
themeSwitchLayout->addStretch();
themeSwitchLayout->addWidget(_themeComboBox);
connect(_themeComboBox, QOverload<int>::of(&ElaComboBox::currentIndexChanged), this, [=](int index) {
if (index == 0)
{
eTheme->setThemeMode(ElaThemeType::Light);
}
else
{
eTheme->setThemeMode(ElaThemeType::Dark);
}
});
connect(eTheme, &ElaTheme::themeModeChanged, this, [=, this](ElaThemeType::ThemeMode themeMode) {
_themeComboBox->blockSignals(true);
if (themeMode == ElaThemeType::Light)
{
_themeComboBox->setCurrentIndex(0);
}
else
{
_themeComboBox->setCurrentIndex(1);
}
_themeComboBox->blockSignals(false);
});
ElaText* helperText = new ElaText("应用程序设置", this);
helperText->setWordWrap(false);
helperText->setTextPixelSize(18);
_micaSwitchButton = new ElaToggleSwitch(this);
ElaScrollPageArea* micaSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* micaSwitchLayout = new QHBoxLayout(micaSwitchArea);
ElaText* micaSwitchText = new ElaText("启用云母效果", this);
micaSwitchText->setWordWrap(false);
micaSwitchText->setTextPixelSize(15);
micaSwitchLayout->addWidget(micaSwitchText);
micaSwitchLayout->addStretch();
micaSwitchLayout->addWidget(_micaSwitchButton);
connect(_micaSwitchButton, &ElaToggleSwitch::toggled, this, [=](bool checked) {
eApp->setIsEnableMica(checked);
});
_logSwitchButton = new ElaToggleSwitch(this);
ElaScrollPageArea* logSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* logSwitchLayout = new QHBoxLayout(logSwitchArea);
ElaText* logSwitchText = new ElaText("启用日志功能", this);
logSwitchText->setWordWrap(false);
logSwitchText->setTextPixelSize(15);
logSwitchLayout->addWidget(logSwitchText);
logSwitchLayout->addStretch();
logSwitchLayout->addWidget(_logSwitchButton);
connect(_logSwitchButton, &ElaToggleSwitch::toggled, this, [=](bool checked) {
ElaLog::getInstance()->initMessageLog(checked);
if (checked)
{
qDebug() << "日志已启用!";
}
else
{
qDebug() << "日志已关闭!";
}
});
_minimumButton = new ElaRadioButton("Minimum", this);
_compactButton = new ElaRadioButton("Compact", this);
_maximumButton = new ElaRadioButton("Maximum", this);
_autoButton = new ElaRadioButton("Auto", this);
_autoButton->setChecked(true);
ElaScrollPageArea* displayModeArea = new ElaScrollPageArea(this);
QHBoxLayout* displayModeLayout = new QHBoxLayout(displayModeArea);
ElaText* displayModeText = new ElaText("导航栏模式选择", this);
displayModeText->setWordWrap(false);
displayModeText->setTextPixelSize(15);
displayModeLayout->addWidget(displayModeText);
displayModeLayout->addStretch();
displayModeLayout->addWidget(_minimumButton);
displayModeLayout->addWidget(_compactButton);
displayModeLayout->addWidget(_maximumButton);
displayModeLayout->addWidget(_autoButton);
connect(_minimumButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Minimal);
}
});
connect(_compactButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Compact);
}
});
connect(_maximumButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Maximal);
}
});
connect(_autoButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Auto);
}
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("UI设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addSpacing(30);
centerLayout->addWidget(themeText);
centerLayout->addSpacing(10);
centerLayout->addWidget(themeSwitchArea);
centerLayout->addSpacing(15);
centerLayout->addWidget(helperText);
centerLayout->addSpacing(10);
centerLayout->addWidget(logSwitchArea);
centerLayout->addWidget(micaSwitchArea);
centerLayout->addWidget(displayModeArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
UISetting::~UISetting()
{
}
+1
View File
@@ -0,0 +1 @@
设置界面,UI使用Ela UI