完善应用设置与聊天状态逻辑

This commit is contained in:
2026-05-30 02:51:01 +08:00
parent 603c408d01
commit 14f1af4b05
10 changed files with 317 additions and 79 deletions
+2 -4
View File
@@ -15,9 +15,10 @@ QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物原型
- `idle` / `talk` / `think` / `sleep` / `happy` / `drag` / `error` 状态
- 托盘显示、隐藏、退出
- 隐藏时暂停动画,显示时恢复动画
- 保存窗口位置置顶状态
- 保存窗口位置置顶、缩放和性能设置
- 文件日志和基础轮转
- 设置窗口
- 应用设置:缩放、性能模式、隐藏暂停、懒加载
- AI Provider 分组配置
- Windows DPAPI 加密保存 API Key
- 非 Windows 环境经用户确认后明文保存 API Key
@@ -32,11 +33,8 @@ QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物原型
尚未实现:
- 设置页内 AI 连通性测试
- 缩放和性能模式 UI
- AppConfig 中缩放 / 性能字段的实际应用
- 角色导入/切换界面
- 对话历史持久化
- 角色包懒加载
- 打包发布脚本
## 技术栈
+3 -4
View File
@@ -1684,8 +1684,7 @@ MIT License 开源
```text
1. 设置页内 AI 连通性测试
2. 对话历史内存上限和可选持久化
3. AppConfig 中缩放、性能模式等字段的实际应用
4. character.json 中 base、anchor、bubble offset 的解析与应用
5. 角色包位置整理、角色切换和懒加载策略
6. 发布前素材授权确认与打包验证
3. character.json 中 base、anchor、bubble offset 的解析与应用
4. 角色包位置整理和角色切换
5. 发布前素材授权确认与打包验证
```
+17 -20
View File
@@ -566,6 +566,8 @@ error 20 帧
已删除临时 AI 测试入口和气泡测试入口
已支持 OpenAI / Google / DeepSeek / Custom 配置分 Provider 保存
已移除废弃 Provider 配置入口,并在读取旧配置时清理废弃 Provider 配置
已支持应用设置页:缩放、性能模式、隐藏暂停、懒加载
已将 AppConfig 的 scale / performanceMode / pauseWhenHidden / enableLazyLoad 接入运行时
Windows 下 API Key 使用 DPAPI 加密保存,非 Windows 需用户确认后才允许明文保存
```
@@ -573,12 +575,10 @@ error 20 帧
```text
1. shiroko 角色包仍位于项目根目录 shiroko/,尚未移动到 resources/characters/shiroko
2. SettingsDialog 仍是最小设置界面,尚未包含 AI 测试按钮、应用设置、角色选择、缩放和性能模式 UI
3. ConfigManager 已有缩放和性能字段,但 PetWindow 尚未真正应用缩放、性能模式和角色选择
4. CharacterPackage 尚未解析并应用 character.json 中的 base、anchor、bubble offset
5. ConversationManager 请求上下文会截取最近 12 条历史,但内存中的 m_history 尚未做最大长度裁剪
6. 当前 FrameAnimator 采用当前角色包全部状态帧预加载,尚未做懒加载
7. README 和开发文档已开始同步当前进度,但仍需随功能继续维护
2. SettingsDialog 仍是最小设置界面,尚未包含 AI 测试按钮、角色选择和更完整的分区布局
3. CharacterPackage 尚未解析并应用 character.json 中的 base、anchor、bubble offset
4. ConversationManager 请求上下文会截取最近 12 条历史,但内存中的 m_history 尚未做最大长度裁剪
5. README 和开发文档已开始同步当前进度,但仍需随功能继续维护
```
---
@@ -600,7 +600,11 @@ error 20 帧
- 长文本流式输出期间应持续 talk
3. 给 ConversationManager 增加内存历史上限,避免长期对话无限增长
4. 把 AI 测试能力迁移到后续设置页,不再放在角色右键菜单
5. 更新设置页结构,为 AI、应用、角色、性能分区预留位置
5. 用户手测应用设置:
- 缩放比例
- 标准 / 低功耗性能模式
- 隐藏到托盘时暂停动画
- 动画懒加载
```
中期建议:
@@ -608,16 +612,10 @@ error 20 帧
```text
1. 完善设置界面:
- AI 配置和测试
- 置顶、缩放、性能模式
- 角色包路径和角色切换
2. 应用 AppConfig 中已有但尚未落地的字段:
- scale
- performanceMode
- pauseWhenHidden
- enableLazyLoad
3. 解析并应用角色包 base / anchor / bubble 配置
4. 评估是否移动 shiroko 到 resources/characters/shiroko
5. 补一轮可重复的稳定性与性能测试记录
2. 解析并应用角色包 base / anchor / bubble 配置
3. 评估是否移动 shiroko 到 resources/characters/shiroko
4. 补一轮可重复的稳定性与性能测试记录
```
---
@@ -628,8 +626,7 @@ error 20 帧
```text
1. 是否把 shiroko 移动到 resources/characters/shiroko
2. 是否保持当前“预加载全部当前角色状态帧”的策略,还是改成按状态懒加载
3. shiroko 素材是否允许作为正式开源发布素材继续保留在仓库中
4. 设置页下一步先做 AI 测试入口,还是先做应用缩放 / 性能设置
5. 是否需要把对话历史持久化保存,还是第一版只保留内存会话
2. shiroko 素材是否允许作为正式开源发布素材继续保留在仓库中
3. 设置页下一步先做 AI 测试入口,还是先做角色包配置
4. 是否需要把对话历史持久化保存,还是第一版只保留内存会话
```
+40 -15
View File
@@ -2,28 +2,19 @@
#include <QSize>
AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize &targetSize)
AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize &targetSize, bool loadFrames)
{
AnimationClip clip;
clip.stateName = state.name;
clip.fps = state.fps;
clip.loop = state.loop;
clip.nextState = state.nextState;
clip.m_framePaths = state.framePaths;
clip.m_targetSize = targetSize;
for (const QString &framePath : state.framePaths)
if (loadFrames)
{
QPixmap pixmap(framePath);
if (pixmap.isNull())
{
continue;
}
if (targetSize.isValid())
{
pixmap = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
clip.m_frames.append(pixmap);
clip.loadFrames();
}
return clip;
@@ -31,7 +22,41 @@ AnimationClip AnimationClip::fromState(const CharacterState &state, const QSize
bool AnimationClip::isValid() const
{
return fps > 0 && !m_frames.isEmpty();
return fps > 0 && (!m_frames.isEmpty() || !m_framePaths.isEmpty());
}
bool AnimationClip::ensureLoaded()
{
if (m_frames.isEmpty())
{
loadFrames();
}
return !m_frames.isEmpty();
}
void AnimationClip::loadFrames()
{
if (!m_frames.isEmpty())
{
return;
}
for (const QString &framePath : m_framePaths)
{
QPixmap pixmap(framePath);
if (pixmap.isNull())
{
continue;
}
if (m_targetSize.isValid())
{
pixmap = pixmap.scaled(m_targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
m_frames.append(pixmap);
}
}
const QPixmap &AnimationClip::frameAt(int index) const
+7 -1
View File
@@ -5,14 +5,16 @@
#include <QPixmap>
#include <QSize>
#include <QString>
#include <QStringList>
#include <QVector>
class AnimationClip
{
public:
static AnimationClip fromState(const CharacterState &state, const QSize &targetSize);
static AnimationClip fromState(const CharacterState &state, const QSize &targetSize, bool loadFrames = true);
bool isValid() const;
bool ensureLoaded();
const QPixmap &frameAt(int index) const;
int frameCount() const;
@@ -22,5 +24,9 @@ public:
QString nextState;
private:
void loadFrames();
QStringList m_framePaths;
QSize m_targetSize;
QVector<QPixmap> m_frames;
};
+38 -10
View File
@@ -2,11 +2,11 @@
#include <QApplication>
#include <QEvent>
#include <QFontMetrics>
#include <QFrame>
#include <QRect>
#include <QScrollBar>
#include <QSize>
#include <QTextDocument>
#include <QVBoxLayout>
#include <QtGlobal>
@@ -18,6 +18,18 @@ constexpr int MaxBubbleHeight = 220;
constexpr int BubbleOffsetY = 8;
constexpr int BubblePaddingWidth = 28;
constexpr int BubblePaddingHeight = 24;
constexpr int BubbleWidthSafetyMargin = 12;
QSizeF documentSizeForWidth(const QTextEdit *textEdit, const QString &message, int width)
{
QTextDocument document;
document.setDefaultFont(textEdit->font());
document.setDefaultTextOption(textEdit->document()->defaultTextOption());
document.setPlainText(message);
document.setDocumentMargin(textEdit->document()->documentMargin());
document.setTextWidth(width);
return document.size();
}
}
ChatBubble::ChatBubble(QWidget *parent)
@@ -78,6 +90,10 @@ void ChatBubble::showMessage(const QString &message, const QPoint &anchorPositio
m_textEdit->setPlainText(trimmed);
const QSize bubbleSize = preferredBubbleSize(trimmed);
const int documentWidth = bubbleSize.width() - BubblePaddingWidth;
const QSizeF documentSize = documentSizeForWidth(m_textEdit, trimmed, documentWidth);
const bool needsVerticalScroll = documentSize.height() + BubblePaddingHeight > MaxBubbleHeight;
m_textEdit->setVerticalScrollBarPolicy(needsVerticalScroll ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
m_textEdit->setFixedSize(bubbleSize);
setFixedSize(bubbleSize);
updatePosition();
@@ -180,17 +196,29 @@ bool ChatBubble::isUserInteractionEvent(QEvent *event) const
QSize ChatBubble::preferredBubbleSize(const QString &message) const
{
const QFontMetrics metrics(m_textEdit->font());
const int textWidth = metrics.horizontalAdvance(message);
const int preferredWidth = qBound(MinBubbleWidth, textWidth + BubblePaddingWidth, MaxBubbleWidth);
QTextDocument singleLineDocument;
singleLineDocument.setDefaultFont(m_textEdit->font());
singleLineDocument.setDefaultTextOption(m_textEdit->document()->defaultTextOption());
singleLineDocument.setPlainText(message);
singleLineDocument.setDocumentMargin(m_textEdit->document()->documentMargin());
singleLineDocument.setTextWidth(-1);
const QRect wrappedRect = metrics.boundingRect(
QRect(0, 0, preferredWidth - BubblePaddingWidth, 10000),
Qt::TextWordWrap,
message);
const int singleLineWidth = qCeil(singleLineDocument.idealWidth())
+ BubblePaddingWidth
+ BubbleWidthSafetyMargin;
int preferredWidth = qBound(MinBubbleWidth, singleLineWidth, MaxBubbleWidth);
const int preferredHeight = qMin(wrappedRect.height() + BubblePaddingHeight, MaxBubbleHeight);
return QSize(preferredWidth, preferredHeight);
QSizeF documentSize = documentSizeForWidth(m_textEdit, message, preferredWidth - BubblePaddingWidth);
int preferredHeight = qCeil(documentSize.height()) + BubblePaddingHeight;
if (preferredHeight > MaxBubbleHeight)
{
const int scrollbarWidth = m_textEdit->verticalScrollBar()->sizeHint().width();
preferredWidth = qMin(MaxBubbleWidth, preferredWidth + scrollbarWidth);
documentSize = documentSizeForWidth(m_textEdit, message, preferredWidth - BubblePaddingWidth - scrollbarWidth);
preferredHeight = qCeil(documentSize.height()) + BubblePaddingHeight;
}
return QSize(preferredWidth, qMin(preferredHeight, MaxBubbleHeight));
}
void ChatBubble::updatePosition()
+143 -21
View File
@@ -28,6 +28,7 @@
#include <QSet>
#include <QStringList>
#include <QVBoxLayout>
#include <QtGlobal>
#include <memory>
@@ -46,6 +47,20 @@ QString previewImagePath()
constexpr int MaxUserMessageLength = 4000;
constexpr int ChatInputLowerOffsetY = 48;
constexpr int StreamBubbleUpdateIntervalMs = 80;
constexpr int BaseAnimationTargetSize = 320;
constexpr int LowPowerFpsCap = 6;
constexpr int ChatFinishedReturnDelayMs = 1500;
AppConfig normalizedAppConfig(AppConfig config)
{
config.scale = qBound(0.5, config.scale, 2.0);
if (config.performanceMode != QStringLiteral("standard")
&& config.performanceMode != QStringLiteral("low-power"))
{
config.performanceMode = QStringLiteral("standard");
}
return config;
}
bool populateRuntimeApiKey(AIConfig &config, QString *errorMessage)
{
@@ -199,17 +214,41 @@ PetWindow::~PetWindow() = default;
void PetWindow::applyAppConfig(const AppConfig &config)
{
setAlwaysOnTop(config.alwaysOnTop);
const AppConfig normalizedConfig = normalizedAppConfig(config);
const bool rebuildClips = !qFuzzyCompare(m_appConfig.scale, normalizedConfig.scale)
|| m_appConfig.performanceMode != normalizedConfig.performanceMode
|| m_appConfig.enableLazyLoad != normalizedConfig.enableLazyLoad;
if (config.hasWindowPosition && isPointVisibleOnScreen(config.windowPosition))
m_appConfig = normalizedConfig;
setAlwaysOnTop(m_appConfig.alwaysOnTop);
if (m_appConfig.hasWindowPosition && isPointVisibleOnScreen(m_appConfig.windowPosition))
{
move(config.windowPosition);
move(m_appConfig.windowPosition);
}
if (rebuildClips && !m_characterPackage.states.isEmpty())
{
const QString previousState = m_stateMachine.currentState().isEmpty()
? QStringLiteral("idle")
: m_stateMachine.currentState();
m_frameAnimator.stop();
buildAnimationClips();
const QString nextState = m_clips.contains(previousState)
? previousState
: QStringLiteral("idle");
if (m_clips.contains(nextState))
{
playResolvedState(m_stateMachine.requestState(nextState), false);
}
}
}
AppConfig PetWindow::currentAppConfig() const
{
AppConfig config;
AppConfig config = m_appConfig;
config.windowPosition = pos();
config.hasWindowPosition = true;
config.alwaysOnTop = m_alwaysOnTop;
@@ -218,6 +257,11 @@ AppConfig PetWindow::currentAppConfig() const
void PetWindow::pauseAnimation()
{
if (!m_appConfig.pauseWhenHidden)
{
return;
}
m_returnToIdleAfterResume = m_behaviorReturnTimer.isActive();
m_idleBehaviorTimer.stop();
m_behaviorReturnTimer.stop();
@@ -294,10 +338,19 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event)
else if (selectedAction == settingsAction)
{
ConfigManager configManager;
SettingsDialog dialog(configManager.loadAIConfigStore(), this);
if (dialog.exec() == QDialog::Accepted && !configManager.saveAIConfigStore(dialog.aiConfigStore()))
SettingsDialog dialog(configManager.loadAIConfigStore(), currentAppConfig(), this);
if (dialog.exec() == QDialog::Accepted)
{
Logger::warning(QStringLiteral("Failed to save AI config from settings dialog."));
if (!configManager.saveAIConfigStore(dialog.aiConfigStore()))
{
Logger::warning(QStringLiteral("Failed to save AI config from settings dialog."));
}
applyAppConfig(dialog.appConfig());
if (!configManager.saveAppConfig(currentAppConfig()))
{
Logger::warning(QStringLiteral("Failed to save app config from settings dialog."));
}
}
}
else if (selectedAction == exitAction)
@@ -306,6 +359,11 @@ void PetWindow::contextMenuEvent(QContextMenuEvent *event)
}
else if (selectedAction != nullptr && selectedAction->data().isValid())
{
if (isManualStateSwitchLocked())
{
return;
}
playState(selectedAction->data().toString(), false);
}
}
@@ -390,10 +448,20 @@ bool PetWindow::submitChatMessage(const QString &message)
window->m_streamBubbleUpdateTimer.stop();
if (response.success)
{
const bool shouldReturnToIdleAfterChat =
window->m_streamingTalkStarted
|| window->m_stateMachine.currentState() == QStringLiteral("think")
|| window->m_stateMachine.currentState() == QStringLiteral("talk")
|| window->m_frameAnimator.currentStateName() == QStringLiteral("think")
|| window->m_frameAnimator.currentStateName() == QStringLiteral("talk");
window->finishStreamingChat();
window->m_streamingAssistantText = response.content;
window->flushStreamingBubble(true);
window->refreshChatHistoryPanel();
if (shouldReturnToIdleAfterChat)
{
window->m_behaviorReturnTimer.start(ChatFinishedReturnDelayMs);
}
return;
}
@@ -442,6 +510,17 @@ bool PetWindow::hasActiveAIRequest() const
return m_conversationManager && m_conversationManager->isBusy();
}
bool PetWindow::isManualStateSwitchLocked() const
{
if (m_streamingChatActive || hasActiveAIRequest())
{
return true;
}
const QString currentState = m_stateMachine.currentState();
return currentState == QStringLiteral("think") || currentState == QStringLiteral("talk");
}
void PetWindow::showConversationHistory()
{
const QVector<ChatMessage> history = m_conversationManager ? m_conversationManager->history() : QVector<ChatMessage>();
@@ -640,10 +719,12 @@ void PetWindow::buildAnimationClips()
m_clips.clear();
QSet<QString> availableStates;
const QSize targetSize(320, 320);
const QSize targetSize = animationTargetSize();
const bool loadFramesImmediately = !m_appConfig.enableLazyLoad;
for (auto iterator = m_characterPackage.states.constBegin(); iterator != m_characterPackage.states.constEnd(); ++iterator)
{
AnimationClip clip = AnimationClip::fromState(iterator.value(), targetSize);
AnimationClip clip = AnimationClip::fromState(iterator.value(), targetSize, loadFramesImmediately);
clip.fps = effectiveAnimationFps(clip.fps);
if (!clip.isValid())
{
continue;
@@ -659,6 +740,7 @@ void PetWindow::buildAnimationClips()
void PetWindow::addStateTestActions(QMenu *menu)
{
QMenu *stateMenu = menu->addMenu(QStringLiteral("State Test"));
const bool stateSwitchLocked = isManualStateSwitchLocked();
const QStringList stateNames = {
QStringLiteral("idle"),
QStringLiteral("talk"),
@@ -680,7 +762,7 @@ void PetWindow::addStateTestActions(QMenu *menu)
stateAction->setData(stateName);
}
stateMenu->setEnabled(!stateMenu->actions().isEmpty());
stateMenu->setEnabled(!stateSwitchLocked && !stateMenu->actions().isEmpty());
}
void PetWindow::updateBubblePosition()
@@ -709,17 +791,17 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
&& stateName != QStringLiteral("error")
&& stateName != QStringLiteral("drag"))
{
const QString heldState = m_streamingTalkStarted
const QString preferredHeldState = m_streamingTalkStarted
? QStringLiteral("talk")
: QStringLiteral("think");
if (stateName != heldState && m_clips.contains(heldState))
{
playResolvedState(heldState, centerWindow);
}
const QString heldState = m_stateMachine.requestState(preferredHeldState, StateRequestSource::System);
if (stateName != heldState)
{
if (!heldState.isEmpty() && m_clips.contains(heldState))
{
playResolvedState(heldState, centerWindow);
}
return;
}
}
@@ -729,13 +811,18 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
return;
}
auto clipIterator = m_clips.constFind(stateName);
if (clipIterator == m_clips.constEnd())
auto clipIterator = m_clips.find(stateName);
if (clipIterator == m_clips.end())
{
return;
}
AnimationClip *clip = &clipIterator.value();
if (!clip->ensureLoaded())
{
return;
}
const AnimationClip *clip = &clipIterator.value();
m_idleBehaviorTimer.stop();
m_behaviorReturnTimer.stop();
m_centerNextFrame = centerWindow;
@@ -751,6 +838,28 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
}
}
QSize PetWindow::animationTargetSize() const
{
const int sideLength = qRound(BaseAnimationTargetSize * m_appConfig.scale);
const int boundedSideLength = qBound(BaseAnimationTargetSize / 2, sideLength, BaseAnimationTargetSize * 2);
return QSize(boundedSideLength, boundedSideLength);
}
int PetWindow::effectiveAnimationFps(int fps) const
{
if (isLowPowerMode())
{
return qMax(1, qMin(fps, LowPowerFpsCap));
}
return qMax(1, fps);
}
bool PetWindow::isLowPowerMode() const
{
return m_appConfig.performanceMode == QStringLiteral("low-power");
}
void PetWindow::scheduleIdleBehavior()
{
if (!m_clips.contains(QStringLiteral("idle")))
@@ -758,7 +867,9 @@ void PetWindow::scheduleIdleBehavior()
return;
}
const int idleDelayMs = QRandomGenerator::global()->bounded(8000, 16001);
const int idleDelayMs = isLowPowerMode()
? QRandomGenerator::global()->bounded(16000, 30001)
: QRandomGenerator::global()->bounded(8000, 16001);
m_idleBehaviorTimer.start(idleDelayMs);
}
@@ -826,15 +937,20 @@ void PetWindow::setDisplayImage(const QString &imagePath, bool centerWindow)
return;
}
const QSize targetSize(320, 320);
const QSize targetSize = animationTargetSize();
const QPixmap scaled = pixmap.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
setDisplayPixmap(scaled, centerWindow);
}
void PetWindow::setDisplayPixmap(const QPixmap &pixmap, bool centerWindow)
{
const QSize previousSize = size();
m_petView->setFrame(pixmap);
resize(pixmap.size());
if (size() != previousSize)
{
updateBubblePosition();
}
if (centerWindow)
{
@@ -862,6 +978,12 @@ bool PetWindow::isPointVisibleOnScreen(const QPoint &point) const
void PetWindow::setAlwaysOnTop(bool enabled)
{
const bool currentlyOnTop = windowFlags().testFlag(Qt::WindowStaysOnTopHint);
if (m_alwaysOnTop == enabled && currentlyOnTop == enabled)
{
return;
}
m_alwaysOnTop = enabled;
const bool wasVisible = isVisible();
+5
View File
@@ -59,12 +59,16 @@ private:
void finishStreamingChat();
void cancelStreamingChat();
bool hasActiveAIRequest() const;
bool isManualStateSwitchLocked() const;
void resetBubbleAutoHideTimer();
QPoint chatInputAnchorPosition() const;
void updateBubblePosition();
QPoint bubbleAnchorPosition() const;
void playState(const QString &stateName, bool centerWindow);
void playResolvedState(const QString &stateName, bool centerWindow);
QSize animationTargetSize() const;
int effectiveAnimationFps(int fps) const;
bool isLowPowerMode() const;
void scheduleIdleBehavior();
void playIdleBehavior();
void returnToIdleFromBehavior();
@@ -81,6 +85,7 @@ private:
QTimer m_idleBehaviorTimer;
QTimer m_behaviorReturnTimer;
QTimer m_streamBubbleUpdateTimer;
AppConfig m_appConfig;
CharacterPackage m_characterPackage;
QMap<QString, AnimationClip> m_clips;
FrameAnimator m_frameAnimator;
+54 -3
View File
@@ -12,7 +12,10 @@
#include <QList>
#include <QPair>
#include <QSpinBox>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QWidget>
#include <QtGlobal>
namespace
{
@@ -28,7 +31,7 @@ QString normalizedProviderName(const QString &provider)
}
}
SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent)
SettingsDialog::SettingsDialog(const AIConfigStore &configStore, const AppConfig &appConfig, QWidget *parent)
: QDialog(parent)
, m_providerComboBox(new QComboBox(this))
, m_baseUrlEdit(new QLineEdit(this))
@@ -39,11 +42,16 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
, m_temperatureSpinBox(new QDoubleSpinBox(this))
, m_maxTokensSpinBox(new QSpinBox(this))
, m_allowPlainApiKeyCheckBox(new QCheckBox(QStringLiteral("允许在非 Windows 环境明文保存 API Key"), this))
, m_scaleSpinBox(new QSpinBox(this))
, m_performanceModeComboBox(new QComboBox(this))
, m_pauseWhenHiddenCheckBox(new QCheckBox(QStringLiteral("隐藏到托盘时暂停动画"), this))
, m_enableLazyLoadCheckBox(new QCheckBox(QStringLiteral("启用动画懒加载"), this))
, m_configStore(configStore)
, m_appConfig(appConfig)
{
setWindowTitle(QStringLiteral("设置"));
setModal(true);
resize(460, 360);
resize(500, 430);
const QList<QPair<QString, QString>> providers = {
{QStringLiteral("openai"), QStringLiteral("OpenAI")},
@@ -72,6 +80,19 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
m_maxTokensSpinBox->setRange(1, 200000);
m_scaleSpinBox->setRange(50, 200);
m_scaleSpinBox->setSingleStep(10);
m_scaleSpinBox->setSuffix(QStringLiteral("%"));
m_scaleSpinBox->setValue(qRound(qBound(0.5, m_appConfig.scale, 2.0) * 100.0));
m_performanceModeComboBox->addItem(QStringLiteral("标准"), QStringLiteral("standard"));
m_performanceModeComboBox->addItem(QStringLiteral("低功耗"), QStringLiteral("low-power"));
const int performanceModeIndex = m_performanceModeComboBox->findData(m_appConfig.performanceMode);
m_performanceModeComboBox->setCurrentIndex(performanceModeIndex >= 0 ? performanceModeIndex : 0);
m_pauseWhenHiddenCheckBox->setChecked(m_appConfig.pauseWhenHidden);
m_enableLazyLoadCheckBox->setChecked(m_appConfig.enableLazyLoad);
m_allowPlainApiKeyCheckBox->setVisible(!SecretStore::isEncryptionAvailable());
m_currentProvider = normalizedProviderName(m_providerComboBox->currentData().toString());
loadProviderConfig(m_currentProvider);
@@ -87,6 +108,22 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
formLayout->addRow(QStringLiteral("Max Tokens"), m_maxTokensSpinBox);
formLayout->addRow(QString(), m_allowPlainApiKeyCheckBox);
auto *aiPage = new QWidget(this);
aiPage->setLayout(formLayout);
auto *appFormLayout = new QFormLayout();
appFormLayout->addRow(QStringLiteral("缩放"), m_scaleSpinBox);
appFormLayout->addRow(QStringLiteral("性能模式"), m_performanceModeComboBox);
appFormLayout->addRow(QString(), m_pauseWhenHiddenCheckBox);
appFormLayout->addRow(QString(), m_enableLazyLoadCheckBox);
auto *appPage = new QWidget(this);
appPage->setLayout(appFormLayout);
auto *tabWidget = new QTabWidget(this);
tabWidget->addTab(aiPage, QStringLiteral("AI"));
tabWidget->addTab(appPage, QStringLiteral("应用"));
auto *storageHintLabel = new QLabel(this);
storageHintLabel->setWordWrap(true);
storageHintLabel->setText(SecretStore::isEncryptionAvailable()
@@ -98,7 +135,7 @@ SettingsDialog::SettingsDialog(const AIConfigStore &configStore, QWidget *parent
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *layout = new QVBoxLayout(this);
layout->addLayout(formLayout);
layout->addWidget(tabWidget);
layout->addWidget(storageHintLabel);
layout->addWidget(buttonBox);
@@ -118,6 +155,20 @@ AIConfigStore SettingsDialog::aiConfigStore() const
return store;
}
AppConfig SettingsDialog::appConfig() const
{
AppConfig config = m_appConfig;
config.scale = m_scaleSpinBox->value() / 100.0;
config.performanceMode = m_performanceModeComboBox->currentData().toString();
if (config.performanceMode.isEmpty())
{
config.performanceMode = QStringLiteral("standard");
}
config.pauseWhenHidden = m_pauseWhenHiddenCheckBox->isChecked();
config.enableLazyLoad = m_enableLazyLoadCheckBox->isChecked();
return config;
}
void SettingsDialog::cacheCurrentProvider()
{
if (m_currentProvider.isEmpty())
+8 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "../config/AIConfig.h"
#include "../config/AppConfig.h"
#include <QDialog>
@@ -13,9 +14,10 @@ class QSpinBox;
class SettingsDialog : public QDialog
{
public:
explicit SettingsDialog(const AIConfigStore &configStore, QWidget *parent = nullptr);
explicit SettingsDialog(const AIConfigStore &configStore, const AppConfig &appConfig, QWidget *parent = nullptr);
AIConfigStore aiConfigStore() const;
AppConfig appConfig() const;
private:
void cacheCurrentProvider();
@@ -33,6 +35,11 @@ private:
QDoubleSpinBox *m_temperatureSpinBox = nullptr;
QSpinBox *m_maxTokensSpinBox = nullptr;
QCheckBox *m_allowPlainApiKeyCheckBox = nullptr;
QSpinBox *m_scaleSpinBox = nullptr;
QComboBox *m_performanceModeComboBox = nullptr;
QCheckBox *m_pauseWhenHiddenCheckBox = nullptr;
QCheckBox *m_enableLazyLoadCheckBox = nullptr;
AIConfigStore m_configStore;
AppConfig m_appConfig;
QString m_currentProvider;
};