完善定时提醒稳定性与管理能力

This commit is contained in:
2026-06-02 22:04:09 +08:00
parent c794e32023
commit 6c2926b57a
20 changed files with 1531 additions and 89 deletions
+25 -4
View File
@@ -35,8 +35,8 @@ QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物项目
- Google Gemini 原生聊天请求 - Google Gemini 原生聊天请求
- 角色文件夹导入和角色切换 - 角色文件夹导入和角色切换
- 删除用户导入角色 - 删除用户导入角色
- 本地一次性提醒:聊天创建、查询、取消,重启后 pending 提醒不丢 - 本地一次性和重复提醒:聊天创建、查询、取消,重启后 pending 提醒不丢
- 提醒到点气泡提示、拖动后延迟提示和隐藏时托盘通知 - 提醒到点气泡提示、稍后提醒、拖动后延迟提示和隐藏时托盘通知
- 提醒音效切换、试听、用户 wav 导入和删除 - 提醒音效切换、试听、用户 wav 导入和删除
- Windows 发布打包脚本和 Inno Setup 安装器脚本 - Windows 发布打包脚本和 Inno Setup 安装器脚本
- Windows GUI 子系统,Release exe 双击不弹控制台窗口 - Windows GUI 子系统,Release exe 双击不弹控制台窗口
@@ -151,7 +151,7 @@ resources/characters/shiroko/
## 定时提醒和音效 ## 定时提醒和音效
当前支持通过聊天输入创建一次性本地提醒,例如: 当前支持通过聊天输入创建一次性和重复本地提醒,例如:
```text ```text
10分钟后提醒我喝水 10分钟后提醒我喝水
@@ -161,6 +161,14 @@ resources/characters/shiroko/
后天9点提醒我开会 后天9点提醒我开会
6月3日9点提醒我提交 6月3日9点提醒我提交
下周一上午10点提醒我周会 下周一上午10点提醒我周会
每天9点提醒我打卡
每天提醒我9点打卡
每日晚上8点提醒我吃药
每周一上午10点提醒我周会
每周一提醒我上午10点周会
每星期五下午3点提醒我提交周报
每月3号9点提醒我交报告
每月3号提醒我9点交报告
提醒列表 提醒列表
取消喝水提醒 取消喝水提醒
``` ```
@@ -171,6 +179,12 @@ resources/characters/shiroko/
QStandardPaths::AppConfigLocation/reminders.json QStandardPaths::AppConfigLocation/reminders.json
``` ```
提醒数据使用原子写入,写入失败时不会触发到点 UI,也不会覆盖旧的有效提醒文件。已触发和已取消记录会写入 `finishedAt`;旧版数据没有该字段时按 `remindAt` 兼容读取。
提醒调度保留最近提醒的精确 timer,同时每 60 秒做一次兜底扫描;程序显示、外部激活或系统睡眠唤醒后,都会重新检查已到期 pending 提醒。
设置页支持编辑 pending 提醒的标题、下一次时间和重复规则;已触发/已取消历史只读。历史记录默认只保留最近 20 天,设置页“清理20天前历史”只删除超过 20 天的已触发/已取消记录,不影响 pending。
提醒文件损坏时会备份为: 提醒文件损坏时会备份为:
```text ```text
@@ -202,8 +216,15 @@ QStandardPaths::AppDataLocation/sounds/reminders/
- 桌宠可见时显示气泡,不发系统通知 - 桌宠可见时显示气泡,不发系统通知
- 桌宠隐藏时发 Windows 托盘通知,不在下次显示时补气泡 - 桌宠隐藏时发 Windows 托盘通知,不在下次显示时补气泡
- AI 正在请求或流式回复时,按隐藏场景处理:播放音效并发 Windows 托盘通知,不显示气泡
- 托盘或系统通知后端不可用时只记录日志,不补气泡
- 用户拖动中不打断 `drag`,拖动结束后显示气泡 - 用户拖动中不打断 `drag`,拖动结束后显示气泡
- 重复提醒尚未支持,包含“每天 / 每周 / 每月”等语义时会提示暂不支持 - 多条提醒同时触发时,可见状态下会按队列逐条展示
- 桌宠可见触发时显示 `知道了``5分钟后再提醒`
- `5分钟后再提醒` 会创建一条新的一次性提醒,不影响原重复规则
- 重复提醒支持 `每天 / 每周 / 每月``工作日 / 每两天 / 每月最后一天 / 自定义间隔 / 农历` 等复杂规则暂不支持
- 每月 31 号这类规则会跳过不存在该日期的月份,寻找下一个有效月份
- 用户音效删除仅允许删除用户音效目录内的安全 sound id,内置音效和非法路径不会被删除
## 配置和日志 ## 配置和日志
@@ -30,7 +30,7 @@
- AI 请求取消和对话清空 - AI 请求取消和对话清空
- 角色文件夹导入和角色切换 - 角色文件夹导入和角色切换
- 删除用户导入角色 - 删除用户导入角色
- 本地一次性提醒、提醒列表、取消提醒和到点通知 - 本地一次性/重复提醒、提醒列表、取消提醒和到点通知
- 内置/用户提醒音效切换、导入、删除和试听 - 内置/用户提醒音效切换、导入、删除和试听
- Windows 打包脚本和 Inno Setup 安装器脚本 - Windows 打包脚本和 Inno Setup 安装器脚本
- Release exe 双击不弹控制台窗口 - Release exe 双击不弹控制台窗口
@@ -366,7 +366,7 @@ CommandDispatcher::dispatch(userText)
## 5.1 功能定位 ## 5.1 功能定位
实现本地一次性提醒功能。 实现本地一次性和基础重复提醒功能。
用户可以输入: 用户可以输入:
@@ -386,12 +386,19 @@ CommandDispatcher::dispatch(userText)
已新增 src/reminder/ 模块 已新增 src/reminder/ 模块
已支持一次性提醒解析、JSON 持久化、启动后加载、到点触发和状态标记 已支持一次性提醒解析、JSON 持久化、启动后加载、到点触发和状态标记
已支持聊天创建 / 查询 / 取消提醒 已支持聊天创建 / 查询 / 取消提醒
已支持设置页按状态查看提醒、取消 pending 提醒、清理已触发/已取消历史 已支持设置页按状态查看提醒、取消 pending 提醒、编辑 pending 提醒、清理 20 天前已触发/已取消历史
已支持 reminder_default / reminder_soft 内置音效 已支持 reminder_default / reminder_soft 内置音效
已支持用户 wav 音效导入、删除、切换和试听 已支持用户 wav 音效导入、删除、切换和试听
已限制用户音效删除路径,只允许删除用户音效目录内的安全 sound id
提醒触发时使用当前设置页选择的全局音效,ReminderItem.soundId 仅保留为历史兼容字段 提醒触发时使用当前设置页选择的全局音效,ReminderItem.soundId 仅保留为历史兼容字段
已接入 Qt Multimedia / QSoundEffect 播放提醒音效 已接入 Qt Multimedia / QSoundEffect 播放提醒音效
已预留 NotificationDispatcher,当前 Windows 仍由托盘通知承接 已预留 NotificationDispatcher,当前 Windows 仍由托盘通知承接
已支持每天 / 每周 / 每月重复提醒
已支持提醒触发后的“知道了”和“5分钟后再提醒”
已支持提醒数据原子保存
已支持多提醒可见队列,避免同时触发时互相覆盖
已支持 60 秒兜底扫描,覆盖睡眠唤醒、系统时间变化和长间隔 timer 延迟场景
通知后端不可用时会记录日志,不补气泡
``` ```
## 5.2 第一版范围 ## 5.2 第一版范围
@@ -400,6 +407,7 @@ CommandDispatcher::dispatch(userText)
```text ```text
一次性提醒 一次性提醒
每天 / 每周 / 每月重复提醒
本地保存 本地保存
程序重启后提醒不丢 程序重启后提醒不丢
到点后触发气泡和托盘通知 到点后触发气泡和托盘通知
@@ -411,7 +419,8 @@ CommandDispatcher::dispatch(userText)
暂不做: 暂不做:
```text ```text
重复提醒 工作日提醒
自定义间隔重复提醒
农历提醒 农历提醒
复杂日历 复杂日历
跨设备同步 跨设备同步
@@ -449,7 +458,20 @@ struct ReminderItem
QDateTime remindAt; QDateTime remindAt;
ReminderStatus status = ReminderStatus::Pending; ReminderStatus status = ReminderStatus::Pending;
QDateTime createdAt; QDateTime createdAt;
QDateTime finishedAt; // triggered/canceled 记录的完成时间;旧数据缺失时按 remindAt 兼容
QString soundId; // 历史兼容字段,触发时不再读取 QString soundId; // 历史兼容字段,触发时不再读取
ReminderRecurrence recurrence;
};
struct ReminderRecurrence
{
ReminderRecurrenceType type = ReminderRecurrenceType::None; // None / Daily / Weekly / Monthly
int interval = 1;
int weekday = 0; // 1-7,仅 weekly 使用
int monthDay = 0; // 1-31,仅 monthly 使用
int hour = -1;
int minute = -1;
QDateTime lastTriggeredAt;
}; };
``` ```
@@ -473,12 +495,26 @@ QStandardPaths::AppConfigLocation/reminders.json
"remindAt": "2026-06-01T20:00:00", "remindAt": "2026-06-01T20:00:00",
"status": "pending", "status": "pending",
"createdAt": "2026-06-01T15:20:00", "createdAt": "2026-06-01T15:20:00",
"soundId": "" "soundId": "",
"recurrence": {
"type": "none",
"interval": 1,
"weekday": 0,
"monthDay": 0,
"hour": -1,
"minute": -1
}
} }
] ]
} }
``` ```
旧版 `reminders.json` 没有 `recurrence` 字段时按一次性提醒读取,继续兼容;没有 `finishedAt` 字段时,已触发/已取消历史按 `remindAt` 作为保留时间兜底。
提醒数据保存使用原子写入。写入失败时创建 / 取消会回滚内存变更,到点触发会保留 pending 并等待下次重试,不播放音效、不通知、不气泡。
历史记录默认只保留最近 20 天。自动清理和设置页“清理20天前历史”只删除超过 20 天的 triggered/canceled 记录,不删除 pending。
配置损坏时备份: 配置损坏时备份:
```text ```text
@@ -511,9 +547,17 @@ reminders.broken.yyyyMMdd-HHmmss.json
1小时后 1小时后
两小时后 两小时后
2小时后 2小时后
每天9点
每天提醒我9点打卡
每日晚上8点
每周一上午10点
每周一提醒我上午10点周会
每星期五下午3点
每月3号9点
每月3号提醒我9点交报告
``` ```
如果规则解析失败,后续可以再接 AI 解析。包含“每天 / 每周 / 每 / 工作日 / 重复”等语义时,当前只返回“重复提醒尚未支持”,不创建一次性提醒。 如果规则解析失败,后续可以再接 AI 解析。当前重复提醒只支持每天 / 每周 / 每月。包含“工作日 / 每两天 / 每月最后一天 / 每季度 / 农历 / 自定义复杂规则”等语义时,返回明确暂不支持提示,不创建一次性提醒。
## 5.7 AI 辅助解析的设计边界 ## 5.7 AI 辅助解析的设计边界
@@ -558,7 +602,12 @@ userText
```text ```text
桌宠可见:播放当前全局音效,显示 ChatBubble + 切 happy,无 happy 时回退 talk,不发 Windows 通知 桌宠可见:播放当前全局音效,显示 ChatBubble + 切 happy,无 happy 时回退 talk,不发 Windows 通知
桌宠隐藏:播放当前全局音效,触发 Windows 托盘通知,不在下次显示时补气泡 桌宠隐藏:播放当前全局音效,触发 Windows 托盘通知,不在下次显示时补气泡
AI 正在请求或流式回复:按隐藏场景处理,播放当前全局音效并发 Windows 通知,不显示气泡,不进入补气泡队列
Windows 托盘通知后端不可用:记录日志,不补气泡,不进入可见队列
用户拖动中:播放当前全局音效,不打断 drag,拖动结束后显示气泡,不发 Windows 通知 用户拖动中:播放当前全局音效,不打断 drag,拖动结束后显示气泡,不发 Windows 通知
多条提醒同时触发:可见状态下进入队列逐条展示,避免后一条覆盖前一条
桌宠可见触发时显示轻量操作区:“知道了”关闭当前提示,“5分钟后再提醒”固定创建新的 5 分钟一次性提醒
重复提醒触发后先推进下一次 remindAt 并保存;稍后提醒不会影响原重复规则
``` ```
提醒文案: 提醒文案:
@@ -1020,8 +1069,8 @@ CustomSearchProvider
## 9.2 第二步:定时提醒 ## 9.2 第二步:定时提醒
```text ```text
当前已落地一次性提醒、提醒列表、取消提醒、到点气泡/托盘通知和提醒音效管理。 当前已落地一次性提醒、每天/每周/每月重复提醒、提醒列表、取消提醒、编辑 pending 提醒、20 天历史保留、到点气泡/托盘通知、稍后提醒和提醒音效管理。
后续可继续补确认/稍后提醒、重复提醒和跨平台通知实现 后续可继续补工作日/自定义间隔、跨平台通知实现和更复杂的提醒管理能力
``` ```
## 9.3 第三步:天气查询 ## 9.3 第三步:天气查询
+1 -1
View File
@@ -77,7 +77,7 @@ int main(int argc, char *argv[])
TrayController trayController(&window); TrayController trayController(&window);
window.setSettingsFallbackInContextMenuEnabled(!trayController.isAvailable()); window.setSettingsFallbackInContextMenuEnabled(!trayController.isAvailable());
window.setTrayNotificationCallback([&trayController](const QString &title, const QString &message) { window.setTrayNotificationCallback([&trayController](const QString &title, const QString &message) {
trayController.showNotification(title, message); return trayController.showNotification(title, message);
}); });
trayController.show(); trayController.show();
+4 -2
View File
@@ -7,10 +7,12 @@ void NotificationDispatcher::setShowCallback(ShowCallback callback)
m_showCallback = std::move(callback); m_showCallback = std::move(callback);
} }
void NotificationDispatcher::showReminder(const QString &title, const QString &message) const bool NotificationDispatcher::showReminder(const QString &title, const QString &message) const
{ {
if (m_showCallback) if (m_showCallback)
{ {
m_showCallback(title, message); return m_showCallback(title, message);
} }
return false;
} }
+2 -2
View File
@@ -7,10 +7,10 @@
class NotificationDispatcher class NotificationDispatcher
{ {
public: public:
using ShowCallback = std::function<void(const QString &, const QString &)>; using ShowCallback = std::function<bool(const QString &, const QString &)>;
void setShowCallback(ShowCallback callback); void setShowCallback(ShowCallback callback);
void showReminder(const QString &title, const QString &message) const; bool showReminder(const QString &title, const QString &message) const;
private: private:
ShowCallback m_showCallback; ShowCallback m_showCallback;
+11 -1
View File
@@ -11,11 +11,21 @@ ReminderCommandResult ReminderCommandHandler::handle(const QString &text, Remind
{ {
ReminderItem item; ReminderItem item;
QString errorMessage; QString errorMessage;
if (!manager.createReminder(command.title, command.originalText, command.remindAt, &item, &errorMessage)) if (!manager.createReminder(command.title, command.originalText, command.remindAt, command.recurrence, &item, &errorMessage))
{ {
return {false, errorMessage.isEmpty() ? QStringLiteral("创建提醒失败。") : errorMessage, {}}; return {false, errorMessage.isEmpty() ? QStringLiteral("创建提醒失败。") : errorMessage, {}};
} }
if (reminderIsRecurring(item))
{
return {
true,
QStringLiteral("已设置重复提醒:%1,规则:%2,下一次:%3")
.arg(item.title, reminderRecurrenceDisplayText(item), reminderDisplayTime(item.remindAt)),
item,
};
}
return { return {
true, true,
QStringLiteral("已设置提醒:%1,时间:%2").arg(item.title, reminderDisplayTime(item.remindAt)), QStringLiteral("已设置提醒:%1,时间:%2").arg(item.title, reminderDisplayTime(item.remindAt)),
+360 -19
View File
@@ -2,9 +2,11 @@
#include "../util/Logger.h" #include "../util/Logger.h"
#include <QDate>
#include <QDateTime> #include <QDateTime>
#include <QRandomGenerator> #include <QRandomGenerator>
#include <QStringList> #include <QStringList>
#include <QTime>
#include <QtGlobal> #include <QtGlobal>
#include <utility> #include <utility>
@@ -13,12 +15,24 @@ namespace
{ {
constexpr qint64 MinimumTimerIntervalMs = 1000; constexpr qint64 MinimumTimerIntervalMs = 1000;
constexpr qint64 MaximumTimerIntervalMs = 24 * 60 * 60 * 1000; constexpr qint64 MaximumTimerIntervalMs = 24 * 60 * 60 * 1000;
constexpr int GuardTimerIntervalMs = 60 * 1000;
constexpr int DefaultHistoryRetentionDays = 20;
bool isPending(const ReminderItem &item) bool isPending(const ReminderItem &item)
{ {
return item.status == ReminderStatus::Pending; return item.status == ReminderStatus::Pending;
} }
bool isFinished(const ReminderItem &item)
{
return item.status == ReminderStatus::Triggered || item.status == ReminderStatus::Canceled;
}
QDateTime finishedReferenceTime(const ReminderItem &item)
{
return item.finishedAt.isValid() ? item.finishedAt : item.remindAt;
}
bool textMatchesReminder(const ReminderItem &item, const QString &query) bool textMatchesReminder(const ReminderItem &item, const QString &query)
{ {
const QString normalizedQuery = query.trimmed(); const QString normalizedQuery = query.trimmed();
@@ -31,6 +45,159 @@ bool textMatchesReminder(const ReminderItem &item, const QString &query)
|| item.title.contains(normalizedQuery, Qt::CaseInsensitive) || item.title.contains(normalizedQuery, Qt::CaseInsensitive)
|| item.originalText.contains(normalizedQuery, Qt::CaseInsensitive); || item.originalText.contains(normalizedQuery, Qt::CaseInsensitive);
} }
QTime recurrenceTime(const ReminderItem &item)
{
const int hour = item.recurrence.hour >= 0 ? item.recurrence.hour : item.remindAt.time().hour();
const int minute = item.recurrence.minute >= 0 ? item.recurrence.minute : item.remindAt.time().minute();
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
{
return {};
}
return QTime(hour, minute);
}
QDateTime nextDailyOccurrence(const ReminderItem &item, const QDateTime &after)
{
const QTime time = recurrenceTime(item);
if (!time.isValid())
{
return {};
}
const int interval = qMax(1, item.recurrence.interval);
QDateTime next(after.date(), time);
while (next <= after)
{
next = next.addDays(interval);
}
return next;
}
QDateTime nextWeeklyOccurrence(const ReminderItem &item, const QDateTime &after)
{
const QTime time = recurrenceTime(item);
if (!time.isValid() || item.recurrence.weekday < 1 || item.recurrence.weekday > 7)
{
return {};
}
const int intervalWeeks = qMax(1, item.recurrence.interval);
int daysToAdd = item.recurrence.weekday - after.date().dayOfWeek();
QDateTime next(after.date().addDays(daysToAdd), time);
while (daysToAdd < 0 || next <= after)
{
next = next.addDays(7 * intervalWeeks);
daysToAdd += 7 * intervalWeeks;
}
return next;
}
QDateTime nextMonthlyOccurrence(const ReminderItem &item, const QDateTime &after)
{
const QTime time = recurrenceTime(item);
if (!time.isValid() || item.recurrence.monthDay < 1 || item.recurrence.monthDay > 31)
{
return {};
}
const int intervalMonths = qMax(1, item.recurrence.interval);
QDate monthCursor(after.date().year(), after.date().month(), 1);
for (int attempt = 0; attempt < 240; ++attempt)
{
const QDate date(monthCursor.year(), monthCursor.month(), item.recurrence.monthDay);
if (date.isValid())
{
const QDateTime next(date, time);
if (next > after)
{
return next;
}
}
monthCursor = monthCursor.addMonths(intervalMonths);
}
return {};
}
QDateTime nextRecurringOccurrence(const ReminderItem &item, const QDateTime &after)
{
switch (item.recurrence.type)
{
case ReminderRecurrenceType::Daily:
return nextDailyOccurrence(item, after);
case ReminderRecurrenceType::Weekly:
return nextWeeklyOccurrence(item, after);
case ReminderRecurrenceType::Monthly:
return nextMonthlyOccurrence(item, after);
case ReminderRecurrenceType::None:
return {};
}
return {};
}
bool normalizeRecurrence(ReminderRecurrence *recurrence, QString *errorMessage)
{
if (recurrence == nullptr || recurrence->type == ReminderRecurrenceType::None)
{
if (recurrence != nullptr)
{
*recurrence = {};
}
return true;
}
recurrence->interval = qMax(1, recurrence->interval);
if (recurrence->hour < 0
|| recurrence->hour > 23
|| recurrence->minute < 0
|| recurrence->minute > 59)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("重复提醒时间无效。");
}
return false;
}
switch (recurrence->type)
{
case ReminderRecurrenceType::Daily:
recurrence->weekday = 0;
recurrence->monthDay = 0;
return true;
case ReminderRecurrenceType::Weekly:
recurrence->monthDay = 0;
if (recurrence->weekday < 1 || recurrence->weekday > 7)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("每周提醒的星期无效。");
}
return false;
}
return true;
case ReminderRecurrenceType::Monthly:
recurrence->weekday = 0;
if (recurrence->monthDay < 1 || recurrence->monthDay > 31)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("每月提醒的日期无效。");
}
return false;
}
return true;
case ReminderRecurrenceType::None:
*recurrence = {};
return true;
}
*recurrence = {};
return true;
}
} }
ReminderManager::ReminderManager() ReminderManager::ReminderManager()
@@ -38,19 +205,26 @@ ReminderManager::ReminderManager()
QObject::connect(&m_timer, &QTimer::timeout, [this]() { QObject::connect(&m_timer, &QTimer::timeout, [this]() {
processDueReminders(); processDueReminders();
}); });
QObject::connect(&m_guardTimer, &QTimer::timeout, [this]() {
checkDueRemindersNow();
});
m_timer.setSingleShot(true); m_timer.setSingleShot(true);
load(); load();
} }
void ReminderManager::start() void ReminderManager::start()
{ {
if (m_started) if (!m_started)
{ {
return; m_started = true;
} }
m_started = true; if (!m_guardTimer.isActive())
processDueReminders(); {
m_guardTimer.start(GuardTimerIntervalMs);
}
checkDueRemindersNow();
} }
void ReminderManager::setTriggeredCallback(TriggeredCallback callback) void ReminderManager::setTriggeredCallback(TriggeredCallback callback)
@@ -81,12 +255,38 @@ ReminderCommand ReminderManager::parseCommand(const QString &text) const
return m_parser.parse(text); return m_parser.parse(text);
} }
void ReminderManager::checkDueRemindersNow()
{
if (!m_started)
{
return;
}
processDueReminders();
QString errorMessage;
if (!pruneFinishedReminders(DefaultHistoryRetentionDays, &errorMessage))
{
Logger::warning(QStringLiteral("Failed to prune old reminder history: ") + errorMessage);
}
}
bool ReminderManager::createReminder( bool ReminderManager::createReminder(
const QString &title, const QString &title,
const QString &originalText, const QString &originalText,
const QDateTime &remindAt, const QDateTime &remindAt,
ReminderItem *createdItem, ReminderItem *createdItem,
QString *errorMessage) QString *errorMessage)
{
return createReminder(title, originalText, remindAt, ReminderRecurrence{}, createdItem, errorMessage);
}
bool ReminderManager::createReminder(
const QString &title,
const QString &originalText,
const QDateTime &remindAt,
const ReminderRecurrence &recurrence,
ReminderItem *createdItem,
QString *errorMessage)
{ {
if (!remindAt.isValid() || remindAt <= QDateTime::currentDateTime()) if (!remindAt.isValid() || remindAt <= QDateTime::currentDateTime())
{ {
@@ -97,6 +297,12 @@ bool ReminderManager::createReminder(
return false; return false;
} }
ReminderRecurrence normalizedRecurrence = recurrence;
if (!normalizeRecurrence(&normalizedRecurrence, errorMessage))
{
return false;
}
ReminderItem item; ReminderItem item;
item.id = nextReminderId(); item.id = nextReminderId();
item.title = title.trimmed().isEmpty() ? QStringLiteral("提醒") : title.trimmed(); item.title = title.trimmed().isEmpty() ? QStringLiteral("提醒") : title.trimmed();
@@ -105,6 +311,11 @@ bool ReminderManager::createReminder(
item.status = ReminderStatus::Pending; item.status = ReminderStatus::Pending;
item.createdAt = QDateTime::currentDateTime(); item.createdAt = QDateTime::currentDateTime();
item.soundId.clear(); item.soundId.clear();
item.recurrence = normalizedRecurrence;
if (!reminderIsRecurring(item.recurrence))
{
item.recurrence = {};
}
m_items.append(item); m_items.append(item);
if (!save(errorMessage)) if (!save(errorMessage))
@@ -122,6 +333,33 @@ bool ReminderManager::createReminder(
return true; return true;
} }
bool ReminderManager::snoozeReminder(
const ReminderItem &sourceItem,
int minutes,
ReminderItem *createdItem,
QString *errorMessage)
{
if (minutes <= 0)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("稍后提醒时间必须晚于当前时间。");
}
return false;
}
const QString originalText = sourceItem.originalText.trimmed().isEmpty()
? sourceItem.title
: sourceItem.originalText;
return createReminder(
sourceItem.title,
originalText,
QDateTime::currentDateTime().addSecs(minutes * 60),
ReminderRecurrence{},
createdItem,
errorMessage);
}
bool ReminderManager::cancelReminder(const QString &id, QString *errorMessage) bool ReminderManager::cancelReminder(const QString &id, QString *errorMessage)
{ {
const QString normalizedId = id.trimmed(); const QString normalizedId = id.trimmed();
@@ -129,11 +367,14 @@ bool ReminderManager::cancelReminder(const QString &id, QString *errorMessage)
{ {
if (item.id == normalizedId && isPending(item)) if (item.id == normalizedId && isPending(item))
{ {
const QDateTime previousFinishedAt = item.finishedAt;
item.status = ReminderStatus::Canceled; item.status = ReminderStatus::Canceled;
item.finishedAt = QDateTime::currentDateTime();
const bool saved = save(errorMessage); const bool saved = save(errorMessage);
if (!saved) if (!saved)
{ {
item.status = ReminderStatus::Pending; item.status = ReminderStatus::Pending;
item.finishedAt = previousFinishedAt;
} }
scheduleNextReminder(); scheduleNextReminder();
return saved; return saved;
@@ -177,12 +418,15 @@ bool ReminderManager::cancelReminderByQuery(const QString &query, ReminderItem *
} }
ReminderItem &item = m_items[matches.first()]; ReminderItem &item = m_items[matches.first()];
const QDateTime previousFinishedAt = item.finishedAt;
item.status = ReminderStatus::Canceled; item.status = ReminderStatus::Canceled;
item.finishedAt = QDateTime::currentDateTime();
const bool saved = save(errorMessage); const bool saved = save(errorMessage);
if (!saved) if (!saved)
{ {
item.status = ReminderStatus::Pending; item.status = ReminderStatus::Pending;
item.finishedAt = previousFinishedAt;
} }
else if (canceledItem != nullptr) else if (canceledItem != nullptr)
{ {
@@ -193,12 +437,19 @@ bool ReminderManager::cancelReminderByQuery(const QString &query, ReminderItem *
} }
bool ReminderManager::clearFinishedReminders(QString *errorMessage) bool ReminderManager::clearFinishedReminders(QString *errorMessage)
{
return pruneFinishedReminders(DefaultHistoryRetentionDays, errorMessage);
}
bool ReminderManager::pruneFinishedReminders(int retentionDays, QString *errorMessage)
{ {
QVector<ReminderItem> previousItems = m_items; QVector<ReminderItem> previousItems = m_items;
const int normalizedRetentionDays = qMax(0, retentionDays);
const QDateTime cutoff = QDateTime::currentDateTime().addDays(-normalizedRetentionDays);
for (int index = m_items.size() - 1; index >= 0; --index) for (int index = m_items.size() - 1; index >= 0; --index)
{ {
const ReminderStatus status = m_items.at(index).status; const ReminderItem &item = m_items.at(index);
if (status == ReminderStatus::Triggered || status == ReminderStatus::Canceled) if (isFinished(item) && finishedReferenceTime(item) < cutoff)
{ {
m_items.removeAt(index); m_items.removeAt(index);
} }
@@ -219,6 +470,78 @@ bool ReminderManager::clearFinishedReminders(QString *errorMessage)
return true; return true;
} }
bool ReminderManager::updateReminder(
const QString &id,
const QString &title,
const QDateTime &remindAt,
const ReminderRecurrence &recurrence,
QString *errorMessage)
{
const QString normalizedId = id.trimmed();
for (ReminderItem &item : m_items)
{
if (item.id != normalizedId || !isPending(item))
{
continue;
}
const ReminderItem previousItem = item;
const QDateTime now = QDateTime::currentDateTime();
ReminderRecurrence normalizedRecurrence = recurrence;
if (!normalizeRecurrence(&normalizedRecurrence, errorMessage))
{
return false;
}
item.title = title.trimmed().isEmpty() ? QStringLiteral("提醒") : title.trimmed();
item.finishedAt = {};
if (reminderIsRecurring(normalizedRecurrence))
{
item.recurrence = normalizedRecurrence;
item.remindAt = nextRecurringOccurrence(item, now);
if (!item.remindAt.isValid())
{
item = previousItem;
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("没有找到有效的重复提醒时间。");
}
return false;
}
}
else
{
if (!remindAt.isValid() || remindAt <= now)
{
item = previousItem;
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("提醒时间必须晚于当前时间。");
}
return false;
}
item.remindAt = remindAt;
item.recurrence = {};
}
if (!save(errorMessage))
{
item = previousItem;
return false;
}
scheduleNextReminder();
return true;
}
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("没有找到可编辑的待提醒事项。");
}
return false;
}
QString ReminderManager::pendingReminderSummary() const QString ReminderManager::pendingReminderSummary() const
{ {
const QVector<ReminderItem> reminders = pendingReminders(); const QVector<ReminderItem> reminders = pendingReminders();
@@ -231,8 +554,8 @@ QString ReminderManager::pendingReminderSummary() const
lines.append(QStringLiteral("当前待提醒:")); lines.append(QStringLiteral("当前待提醒:"));
for (const ReminderItem &item : reminders) for (const ReminderItem &item : reminders)
{ {
lines.append(QStringLiteral("%1%2%3") lines.append(QStringLiteral("%1%2%3,下一次:%4")
.arg(item.id, item.title, reminderDisplayTime(item.remindAt))); .arg(item.id, item.title, reminderRecurrenceDisplayText(item), reminderDisplayTime(item.remindAt)));
} }
return lines.join(QChar('\n')); return lines.join(QChar('\n'));
} }
@@ -256,15 +579,39 @@ void ReminderManager::processDueReminders()
{ {
const QDateTime now = QDateTime::currentDateTime(); const QDateTime now = QDateTime::currentDateTime();
QVector<ReminderItem> triggeredItems; QVector<ReminderItem> triggeredItems;
QVector<int> triggeredIndexes; const QVector<ReminderItem> previousItems = m_items;
for (int index = 0; index < m_items.size(); ++index) for (int index = 0; index < m_items.size(); ++index)
{ {
ReminderItem &item = m_items[index]; ReminderItem &item = m_items[index];
if (isPending(item) && item.remindAt <= now) if (isPending(item) && item.remindAt <= now)
{ {
item.status = ReminderStatus::Triggered; ReminderItem triggeredItem = item;
triggeredItems.append(item); triggeredItem.status = ReminderStatus::Triggered;
triggeredIndexes.append(index); triggeredItem.finishedAt = now;
triggeredItem.recurrence.lastTriggeredAt = now;
if (reminderIsRecurring(item))
{
const QDateTime nextOccurrence = nextRecurringOccurrence(item, now);
if (!nextOccurrence.isValid())
{
Logger::warning(QStringLiteral("Failed to compute next recurring reminder occurrence: id=%1").arg(item.id));
item.status = ReminderStatus::Triggered;
item.finishedAt = now;
}
else
{
item.recurrence.lastTriggeredAt = now;
item.remindAt = nextOccurrence;
item.finishedAt = {};
}
}
else
{
item.status = ReminderStatus::Triggered;
item.finishedAt = now;
}
triggeredItems.append(triggeredItem);
} }
} }
@@ -274,13 +621,7 @@ void ReminderManager::processDueReminders()
if (!save(&errorMessage)) if (!save(&errorMessage))
{ {
Logger::warning(QStringLiteral("Failed to save triggered reminders: ") + errorMessage); Logger::warning(QStringLiteral("Failed to save triggered reminders: ") + errorMessage);
for (const int index : triggeredIndexes) m_items = previousItems;
{
if (index >= 0 && index < m_items.size())
{
m_items[index].status = ReminderStatus::Pending;
}
}
scheduleNextReminder(); scheduleNextReminder();
return; return;
} }
+21
View File
@@ -21,6 +21,7 @@ public:
QVector<ReminderItem> allReminders() const; QVector<ReminderItem> allReminders() const;
QVector<ReminderItem> pendingReminders() const; QVector<ReminderItem> pendingReminders() const;
ReminderCommand parseCommand(const QString &text) const; ReminderCommand parseCommand(const QString &text) const;
void checkDueRemindersNow();
bool createReminder( bool createReminder(
const QString &title, const QString &title,
@@ -28,9 +29,28 @@ public:
const QDateTime &remindAt, const QDateTime &remindAt,
ReminderItem *createdItem = nullptr, ReminderItem *createdItem = nullptr,
QString *errorMessage = nullptr); QString *errorMessage = nullptr);
bool createReminder(
const QString &title,
const QString &originalText,
const QDateTime &remindAt,
const ReminderRecurrence &recurrence,
ReminderItem *createdItem = nullptr,
QString *errorMessage = nullptr);
bool snoozeReminder(
const ReminderItem &sourceItem,
int minutes,
ReminderItem *createdItem = nullptr,
QString *errorMessage = nullptr);
bool cancelReminder(const QString &id, QString *errorMessage = nullptr); bool cancelReminder(const QString &id, QString *errorMessage = nullptr);
bool cancelReminderByQuery(const QString &query, ReminderItem *canceledItem = nullptr, QString *errorMessage = nullptr); bool cancelReminderByQuery(const QString &query, ReminderItem *canceledItem = nullptr, QString *errorMessage = nullptr);
bool clearFinishedReminders(QString *errorMessage = nullptr); bool clearFinishedReminders(QString *errorMessage = nullptr);
bool pruneFinishedReminders(int retentionDays = 20, QString *errorMessage = nullptr);
bool updateReminder(
const QString &id,
const QString &title,
const QDateTime &remindAt,
const ReminderRecurrence &recurrence,
QString *errorMessage = nullptr);
QString pendingReminderSummary() const; QString pendingReminderSummary() const;
private: private:
@@ -44,6 +64,7 @@ private:
ReminderParser m_parser; ReminderParser m_parser;
QVector<ReminderItem> m_items; QVector<ReminderItem> m_items;
QTimer m_timer; QTimer m_timer;
QTimer m_guardTimer;
TriggeredCallback m_triggeredCallback; TriggeredCallback m_triggeredCallback;
bool m_started = false; bool m_started = false;
}; };
+247 -15
View File
@@ -182,6 +182,13 @@ struct ReminderDateResolution
bool explicitDate = false; bool explicitDate = false;
}; };
struct ParsedReminderTime
{
int hour = -1;
int minute = -1;
QString expression;
};
ReminderDateResolution resolveReminderDate(const QString &text, const QDate &currentDate) ReminderDateResolution resolveReminderDate(const QString &text, const QDate &currentDate)
{ {
QRegularExpressionMatch match; QRegularExpressionMatch match;
@@ -254,6 +261,134 @@ QString removeFirst(const QString &text, const QString &part)
} }
return result; return result;
} }
ReminderCommand invalidRecurringCommand(const QString &text, const QString &message)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, message};
}
ReminderCommand createReminderCommand(
const QString &title,
const QString &originalText,
const QDateTime &remindAt,
const ReminderRecurrence &recurrence = {})
{
ReminderCommand command;
command.type = ReminderCommandType::Create;
command.title = title;
command.originalText = originalText;
command.remindAt = remindAt;
command.recurrence = recurrence;
return command;
}
ParsedReminderTime parsedTimeFromPointMatch(const QRegularExpressionMatch &match, int periodIndex, int hourIndex, int minuteIndex)
{
const QString period = match.captured(periodIndex);
const int hour = adjustedHour(period, parseSmallInteger(match.captured(hourIndex)));
const int minute = match.captured(minuteIndex).isEmpty() ? 0 : match.captured(minuteIndex).toInt();
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
{
return {};
}
return {hour, minute, match.captured(0)};
}
ParsedReminderTime parsedTimeFromColonMatch(const QRegularExpressionMatch &match, int hourIndex, int minuteIndex)
{
const int hour = match.captured(hourIndex).toInt();
const int minute = match.captured(minuteIndex).toInt();
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
{
return {};
}
return {hour, minute, match.captured(0)};
}
ParsedReminderTime parseTimeExpression(const QString &text)
{
QRegularExpressionMatch match;
const QRegularExpression pointExpression(QStringLiteral("(上午|早上|下午|晚上|中午|凌晨)?\\s*([0-9]{1,2}|[一二两三四五六七八九十]+)\\s*点\\s*(?:(\\d{1,2})\\s*分?)?"));
match = pointExpression.match(text);
if (match.hasMatch())
{
return parsedTimeFromPointMatch(match, 1, 2, 3);
}
const QRegularExpression colonExpression(QStringLiteral("([01]?\\d|2[0-3])\\s*[:]\\s*([0-5]\\d)"));
match = colonExpression.match(text);
if (match.hasMatch())
{
return parsedTimeFromColonMatch(match, 1, 2);
}
return {};
}
QDateTime nextDailyOccurrence(const QDateTime &now, const QTime &time)
{
QDateTime next(now.date(), time);
if (next <= now)
{
next = next.addDays(1);
}
return next;
}
QDateTime nextWeeklyOccurrence(const QDateTime &now, int weekday, const QTime &time)
{
int daysToAdd = weekday - now.date().dayOfWeek();
QDateTime next(now.date().addDays(daysToAdd), time);
if (daysToAdd < 0 || next <= now)
{
next = next.addDays(7);
}
return next;
}
QDateTime nextMonthlyOccurrence(const QDateTime &now, int monthDay, const QTime &time)
{
if (monthDay < 1 || monthDay > 31)
{
return {};
}
QDate monthCursor(now.date().year(), now.date().month(), 1);
for (int attempt = 0; attempt < 240; ++attempt)
{
const QDate date(monthCursor.year(), monthCursor.month(), monthDay);
if (date.isValid())
{
const QDateTime next(date, time);
if (next > now)
{
return next;
}
}
monthCursor = monthCursor.addMonths(1);
}
return {};
}
bool containsUnsupportedRecurrence(const QString &text)
{
static const QRegularExpression intervalExpression(QStringLiteral("\\s*([0-9]+|[一二两三四五六七八九十]+)\\s*(天|周|星期|月)"));
return containsAny(text, {
QStringLiteral("每年"),
QStringLiteral("工作日"),
QStringLiteral("重复"),
QStringLiteral("每隔"),
QStringLiteral("隔天"),
QStringLiteral("每季度"),
QStringLiteral("每季"),
QStringLiteral("每小时"),
QStringLiteral("每分钟"),
QStringLiteral("农历"),
}) || intervalExpression.match(text).hasMatch();
}
} }
ReminderCommand ReminderParser::parse(const QString &text, const QDateTime &now) const ReminderCommand ReminderParser::parse(const QString &text, const QDateTime &now) const
@@ -293,18 +428,15 @@ ReminderCommand ReminderParser::parseCreateCommand(const QString &text, const QD
{ {
const QDate currentDate = now.date(); const QDate currentDate = now.date();
if (containsAny(text, { const ReminderCommand recurringCommand = parseRecurringCreateCommand(text, now);
QStringLiteral("每天"), if (recurringCommand.type != ReminderCommandType::Invalid || !recurringCommand.errorMessage.isEmpty())
QStringLiteral("每日"),
QStringLiteral("每周"),
QStringLiteral("每星期"),
QStringLiteral("每月"),
QStringLiteral("每年"),
QStringLiteral("工作日"),
QStringLiteral("重复"),
}))
{ {
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("重复提醒尚未支持,目前只能创建一次性提醒。")}; return recurringCommand;
}
if (containsUnsupportedRecurrence(text))
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("暂不支持该重复提醒规则,目前支持每天、每周、每月。")};
} }
const QRegularExpression relativeMinutesExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*")); const QRegularExpression relativeMinutesExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*"));
@@ -448,9 +580,108 @@ ReminderCommand ReminderParser::parseCreateCommand(const QString &text, const QD
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到提醒时间。支持如“10分钟后提醒我喝水”“明天9点提醒我开会”。")}; return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到提醒时间。支持如“10分钟后提醒我喝水”“明天9点提醒我开会”。")};
} }
ReminderCommand ReminderParser::parseRecurringCreateCommand(const QString &text, const QDateTime &now) const
{
QRegularExpressionMatch match;
if (text.contains(QStringLiteral("每月"))
&& (text.contains(QStringLiteral("最后")) || text.contains(QStringLiteral("月末"))))
{
return invalidRecurringCommand(text, QStringLiteral("暂不支持每月最后一天提醒,目前支持每月具体日期。"));
}
if (text.contains(QStringLiteral("每天")) || text.contains(QStringLiteral("每日")))
{
const ParsedReminderTime parsedTime = parseTimeExpression(text);
if (parsedTime.hour < 0)
{
return invalidRecurringCommand(text, QStringLiteral("没有识别到重复提醒时间。支持如“每天9点提醒我打卡”。"));
}
const QTime time(parsedTime.hour, parsedTime.minute);
ReminderRecurrence recurrence;
recurrence.type = ReminderRecurrenceType::Daily;
recurrence.interval = 1;
recurrence.hour = parsedTime.hour;
recurrence.minute = parsedTime.minute;
return createReminderCommand(
extractTitle(text, parsedTime.expression),
text,
nextDailyOccurrence(now, time),
recurrence);
}
if (text.contains(QStringLiteral("每周")) || text.contains(QStringLiteral("每星期")))
{
const QRegularExpression weeklyExpression(QStringLiteral("(?:每周|每星期)\\s*([1-7])"));
match = weeklyExpression.match(text);
const int weekday = match.hasMatch() ? weekdayFromText(match.captured(1)) : -1;
const ParsedReminderTime parsedTime = parseTimeExpression(text);
if (weekday < 1 || parsedTime.hour < 0)
{
return invalidRecurringCommand(text, QStringLiteral("没有识别到重复提醒时间。支持如“每周一上午10点提醒我周会”。"));
}
const QTime time(parsedTime.hour, parsedTime.minute);
ReminderRecurrence recurrence;
recurrence.type = ReminderRecurrenceType::Weekly;
recurrence.interval = 1;
recurrence.weekday = weekday;
recurrence.hour = parsedTime.hour;
recurrence.minute = parsedTime.minute;
return createReminderCommand(
extractTitle(text, parsedTime.expression),
text,
nextWeeklyOccurrence(now, weekday, time),
recurrence);
}
if (text.contains(QStringLiteral("每月")))
{
const QRegularExpression monthlyExpression(QStringLiteral("每月\\s*([0-9]{1,2}|[一二两三四五六七八九十]+)\\s*(?:|)?"));
match = monthlyExpression.match(text);
const int monthDay = match.hasMatch() ? parseSmallInteger(match.captured(1)) : -1;
const ParsedReminderTime parsedTime = parseTimeExpression(text);
if (monthDay < 1 || monthDay > 31 || parsedTime.hour < 0)
{
return invalidRecurringCommand(text, QStringLiteral("没有识别到重复提醒时间。支持如“每月3号9点提醒我交报告”。"));
}
const QTime time(parsedTime.hour, parsedTime.minute);
const QDateTime remindAt = nextMonthlyOccurrence(now, monthDay, time);
if (!remindAt.isValid())
{
return invalidRecurringCommand(text, QStringLiteral("没有找到有效的每月提醒日期。"));
}
ReminderRecurrence recurrence;
recurrence.type = ReminderRecurrenceType::Monthly;
recurrence.interval = 1;
recurrence.monthDay = monthDay;
recurrence.hour = parsedTime.hour;
recurrence.minute = parsedTime.minute;
return createReminderCommand(
extractTitle(text, parsedTime.expression),
text,
remindAt,
recurrence);
}
return {};
}
QString ReminderParser::extractTitle(QString text, const QString &timeExpression) const QString ReminderParser::extractTitle(QString text, const QString &timeExpression) const
{ {
text = removeFirst(text, timeExpression); text = removeFirst(text, timeExpression);
text.remove(QRegularExpression(QStringLiteral("\\d{1,2}\\s*月\\s*\\d{1,2}\\s*(?:日|号)?")));
text.remove(QRegularExpression(QStringLiteral("\\d{1,2}\\s*/\\s*\\d{1,2}")));
text.remove(QRegularExpression(QStringLiteral("下周\\s*[一二三四五六日天1-7]")));
text.remove(QRegularExpression(QStringLiteral("每周\\s*[一二三四五六日天1-7]")));
text.remove(QRegularExpression(QStringLiteral("每星期\\s*[一二三四五六日天1-7]")));
text.remove(QRegularExpression(QStringLiteral("每月\\s*([0-9]{1,2}|[一二两三四五六七八九十]+)\\s*(?:日|号)?")));
const QStringList tokensToRemove = { const QStringList tokensToRemove = {
QStringLiteral("提醒我"), QStringLiteral("提醒我"),
QStringLiteral("提醒"), QStringLiteral("提醒"),
@@ -462,6 +693,11 @@ QString ReminderParser::extractTitle(QString text, const QString &timeExpression
QStringLiteral("今天"), QStringLiteral("今天"),
QStringLiteral("明天"), QStringLiteral("明天"),
QStringLiteral("后天"), QStringLiteral("后天"),
QStringLiteral("每天"),
QStringLiteral("每日"),
QStringLiteral("每周"),
QStringLiteral("每星期"),
QStringLiteral("每月"),
}; };
for (const QString &token : tokensToRemove) for (const QString &token : tokensToRemove)
@@ -469,10 +705,6 @@ QString ReminderParser::extractTitle(QString text, const QString &timeExpression
text.remove(token); text.remove(token);
} }
text.remove(QRegularExpression(QStringLiteral("\\d{1,2}\\s*月\\s*\\d{1,2}\\s*(?:日|号)?")));
text.remove(QRegularExpression(QStringLiteral("\\d{1,2}\\s*/\\s*\\d{1,2}")));
text.remove(QRegularExpression(QStringLiteral("下周\\s*[一二三四五六日天1-7]")));
text = text.trimmed(); text = text.trimmed();
while (text.startsWith(QChar(0x3000)) || text.startsWith(QLatin1Char(' ')) || text.startsWith(QLatin1Char(',')) || text.startsWith(QChar(0xff0c))) while (text.startsWith(QChar(0x3000)) || text.startsWith(QLatin1Char(' ')) || text.startsWith(QLatin1Char(',')) || text.startsWith(QChar(0xff0c)))
{ {
+1
View File
@@ -12,5 +12,6 @@ public:
private: private:
ReminderCommand parseCreateCommand(const QString &text, const QDateTime &now) const; ReminderCommand parseCreateCommand(const QString &text, const QDateTime &now) const;
ReminderCommand parseRecurringCreateCommand(const QString &text, const QDateTime &now) const;
QString extractTitle(QString text, const QString &timeExpression) const; QString extractTitle(QString text, const QString &timeExpression) const;
}; };
+36 -1
View File
@@ -73,6 +73,22 @@ bool readChunkHeader(QDataStream &stream, QByteArray *id, quint32 *size)
stream >> *size; stream >> *size;
return stream.status() == QDataStream::Ok; return stream.status() == QDataStream::Ok;
} }
bool isSafeSoundId(const QString &soundId)
{
static const QRegularExpression expression(QStringLiteral("^[A-Za-z0-9][A-Za-z0-9._-]*$"));
return expression.match(soundId).hasMatch()
&& !soundId.contains(QLatin1Char('/'))
&& !soundId.contains(QLatin1Char('\\'));
}
bool pathStaysInsideDirectory(const QString &filePath, const QString &directoryPath)
{
const QString absoluteFilePath = QDir::cleanPath(QFileInfo(filePath).absoluteFilePath());
const QString absoluteDirectoryPath = QDir::cleanPath(QDir(directoryPath).absolutePath());
return absoluteFilePath == absoluteDirectoryPath
|| absoluteFilePath.startsWith(absoluteDirectoryPath + QLatin1Char('/'));
}
} }
QString ReminderSoundRepository::defaultSoundId() QString ReminderSoundRepository::defaultSoundId()
@@ -227,7 +243,26 @@ bool ReminderSoundRepository::deleteUserSound(const QString &soundId, QString *e
return false; return false;
} }
const QString path = QDir(userSoundsRootPath()).filePath(id + QStringLiteral(".wav")); if (!isSafeSoundId(id))
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("音效 id 不安全。");
}
return false;
}
const QString userRootPath = userSoundsRootPath();
const QString path = QDir(userRootPath).filePath(id + QStringLiteral(".wav"));
if (!pathStaysInsideDirectory(path, userRootPath))
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("音效路径不安全。");
}
return false;
}
QFile file(path); QFile file(path);
if (!file.exists()) if (!file.exists())
{ {
+109 -3
View File
@@ -10,12 +10,80 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonParseError> #include <QJsonParseError>
#include <QSaveFile>
#include <QStandardPaths> #include <QStandardPaths>
#include <QtGlobal>
namespace namespace
{ {
const QString ReminderStoreFileName = QStringLiteral("reminders.json"); const QString ReminderStoreFileName = QStringLiteral("reminders.json");
QJsonObject recurrenceToObject(const ReminderRecurrence &recurrence)
{
QJsonObject object;
object.insert(QStringLiteral("type"), reminderRecurrenceTypeToString(recurrence.type));
object.insert(QStringLiteral("interval"), qMax(1, recurrence.interval));
object.insert(QStringLiteral("weekday"), recurrence.weekday);
object.insert(QStringLiteral("monthDay"), recurrence.monthDay);
object.insert(QStringLiteral("hour"), recurrence.hour);
object.insert(QStringLiteral("minute"), recurrence.minute);
if (recurrence.lastTriggeredAt.isValid())
{
object.insert(QStringLiteral("lastTriggeredAt"), recurrence.lastTriggeredAt.toString(Qt::ISODate));
}
return object;
}
ReminderRecurrence recurrenceFromObject(const QJsonObject &object)
{
ReminderRecurrence recurrence;
recurrence.type = reminderRecurrenceTypeFromString(object.value(QStringLiteral("type")).toString());
recurrence.interval = qMax(1, object.value(QStringLiteral("interval")).toInt(1));
recurrence.weekday = object.value(QStringLiteral("weekday")).toInt(0);
recurrence.monthDay = object.value(QStringLiteral("monthDay")).toInt(0);
recurrence.hour = object.value(QStringLiteral("hour")).toInt(-1);
recurrence.minute = object.value(QStringLiteral("minute")).toInt(-1);
recurrence.lastTriggeredAt = QDateTime::fromString(
object.value(QStringLiteral("lastTriggeredAt")).toString(),
Qt::ISODate);
if (recurrence.type == ReminderRecurrenceType::None)
{
recurrence = {};
}
return recurrence;
}
bool recurrenceIsValid(const ReminderRecurrence &recurrence)
{
if (recurrence.type == ReminderRecurrenceType::None)
{
return true;
}
if (recurrence.interval < 1
|| recurrence.hour < 0
|| recurrence.hour > 23
|| recurrence.minute < 0
|| recurrence.minute > 59)
{
return false;
}
if (recurrence.type == ReminderRecurrenceType::Weekly)
{
return recurrence.weekday >= 1 && recurrence.weekday <= 7;
}
if (recurrence.type == ReminderRecurrenceType::Monthly)
{
return recurrence.monthDay >= 1 && recurrence.monthDay <= 31;
}
return recurrence.type == ReminderRecurrenceType::Daily;
}
QJsonObject reminderToObject(const ReminderItem &item) QJsonObject reminderToObject(const ReminderItem &item)
{ {
QJsonObject object; QJsonObject object;
@@ -25,7 +93,12 @@ QJsonObject reminderToObject(const ReminderItem &item)
object.insert(QStringLiteral("remindAt"), item.remindAt.toString(Qt::ISODate)); object.insert(QStringLiteral("remindAt"), item.remindAt.toString(Qt::ISODate));
object.insert(QStringLiteral("status"), reminderStatusToString(item.status)); object.insert(QStringLiteral("status"), reminderStatusToString(item.status));
object.insert(QStringLiteral("createdAt"), item.createdAt.toString(Qt::ISODate)); object.insert(QStringLiteral("createdAt"), item.createdAt.toString(Qt::ISODate));
if (item.finishedAt.isValid())
{
object.insert(QStringLiteral("finishedAt"), item.finishedAt.toString(Qt::ISODate));
}
object.insert(QStringLiteral("soundId"), item.soundId); object.insert(QStringLiteral("soundId"), item.soundId);
object.insert(QStringLiteral("recurrence"), recurrenceToObject(item.recurrence));
return object; return object;
} }
@@ -38,7 +111,14 @@ ReminderItem reminderFromObject(const QJsonObject &object)
item.remindAt = QDateTime::fromString(object.value(QStringLiteral("remindAt")).toString(), Qt::ISODate); item.remindAt = QDateTime::fromString(object.value(QStringLiteral("remindAt")).toString(), Qt::ISODate);
item.status = reminderStatusFromString(object.value(QStringLiteral("status")).toString()); item.status = reminderStatusFromString(object.value(QStringLiteral("status")).toString());
item.createdAt = QDateTime::fromString(object.value(QStringLiteral("createdAt")).toString(), Qt::ISODate); item.createdAt = QDateTime::fromString(object.value(QStringLiteral("createdAt")).toString(), Qt::ISODate);
item.finishedAt = QDateTime::fromString(object.value(QStringLiteral("finishedAt")).toString(), Qt::ISODate);
item.soundId = object.value(QStringLiteral("soundId")).toString().trimmed(); item.soundId = object.value(QStringLiteral("soundId")).toString().trimmed();
item.recurrence = recurrenceFromObject(object.value(QStringLiteral("recurrence")).toObject());
if (!recurrenceIsValid(item.recurrence))
{
Logger::warning(QStringLiteral("Invalid reminder recurrence downgraded to one-shot: id=%1").arg(item.id));
item.recurrence = {};
}
return item; return item;
} }
} }
@@ -98,6 +178,12 @@ QVector<ReminderItem> ReminderStore::load(QString *errorMessage) const
item.createdAt = item.remindAt; item.createdAt = item.remindAt;
} }
if (!item.finishedAt.isValid()
&& (item.status == ReminderStatus::Triggered || item.status == ReminderStatus::Canceled))
{
item.finishedAt = item.remindAt;
}
items.append(item); items.append(item);
} }
@@ -125,8 +211,8 @@ bool ReminderStore::save(const QVector<ReminderItem> &items, QString *errorMessa
QJsonObject root; QJsonObject root;
root.insert(QStringLiteral("reminders"), reminders); root.insert(QStringLiteral("reminders"), reminders);
QFile file(storePath()); QSaveFile file(storePath());
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) if (!file.open(QIODevice::WriteOnly))
{ {
if (errorMessage != nullptr) if (errorMessage != nullptr)
{ {
@@ -136,7 +222,27 @@ bool ReminderStore::save(const QVector<ReminderItem> &items, QString *errorMessa
} }
const QJsonDocument document(root); const QJsonDocument document(root);
return file.write(document.toJson(QJsonDocument::Indented)) >= 0; const QByteArray payload = document.toJson(QJsonDocument::Indented);
if (file.write(payload) != payload.size())
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("写入提醒文件不完整。");
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("提交提醒文件失败。");
}
return false;
}
return true;
} }
QString ReminderStore::storePath() const QString ReminderStore::storePath() const
+103
View File
@@ -2,6 +2,44 @@
#include <algorithm> #include <algorithm>
namespace
{
QString twoDigit(int value)
{
return QString::number(value).rightJustified(2, QLatin1Char('0'));
}
QString weekdayDisplayText(int weekday)
{
switch (weekday)
{
case 1:
return QStringLiteral("");
case 2:
return QStringLiteral("");
case 3:
return QStringLiteral("");
case 4:
return QStringLiteral("");
case 5:
return QStringLiteral("");
case 6:
return QStringLiteral("");
case 7:
return QStringLiteral("");
default:
return QStringLiteral("?");
}
}
QString recurrenceTimeText(const ReminderItem &item)
{
const int hour = item.recurrence.hour >= 0 ? item.recurrence.hour : item.remindAt.time().hour();
const int minute = item.recurrence.minute >= 0 ? item.recurrence.minute : item.remindAt.time().minute();
return QStringLiteral("%1:%2").arg(twoDigit(hour), twoDigit(minute));
}
}
QString reminderStatusToString(ReminderStatus status) QString reminderStatusToString(ReminderStatus status)
{ {
switch (status) switch (status)
@@ -33,11 +71,76 @@ ReminderStatus reminderStatusFromString(const QString &status)
return ReminderStatus::Pending; return ReminderStatus::Pending;
} }
QString reminderRecurrenceTypeToString(ReminderRecurrenceType type)
{
switch (type)
{
case ReminderRecurrenceType::None:
return QStringLiteral("none");
case ReminderRecurrenceType::Daily:
return QStringLiteral("daily");
case ReminderRecurrenceType::Weekly:
return QStringLiteral("weekly");
case ReminderRecurrenceType::Monthly:
return QStringLiteral("monthly");
}
return QStringLiteral("none");
}
ReminderRecurrenceType reminderRecurrenceTypeFromString(const QString &type)
{
const QString normalized = type.trimmed().toLower();
if (normalized == QStringLiteral("daily"))
{
return ReminderRecurrenceType::Daily;
}
if (normalized == QStringLiteral("weekly"))
{
return ReminderRecurrenceType::Weekly;
}
if (normalized == QStringLiteral("monthly"))
{
return ReminderRecurrenceType::Monthly;
}
return ReminderRecurrenceType::None;
}
bool reminderIsRecurring(const ReminderItem &item)
{
return reminderIsRecurring(item.recurrence);
}
bool reminderIsRecurring(const ReminderRecurrence &recurrence)
{
return recurrence.type != ReminderRecurrenceType::None;
}
QString reminderDisplayTime(const QDateTime &dateTime) QString reminderDisplayTime(const QDateTime &dateTime)
{ {
return dateTime.toLocalTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")); return dateTime.toLocalTime().toString(QStringLiteral("yyyy-MM-dd HH:mm"));
} }
QString reminderRecurrenceDisplayText(const ReminderItem &item)
{
switch (item.recurrence.type)
{
case ReminderRecurrenceType::None:
return QStringLiteral("一次");
case ReminderRecurrenceType::Daily:
return QStringLiteral("每天 %1").arg(recurrenceTimeText(item));
case ReminderRecurrenceType::Weekly:
return QStringLiteral("每周%1 %2").arg(weekdayDisplayText(item.recurrence.weekday), recurrenceTimeText(item));
case ReminderRecurrenceType::Monthly:
return QStringLiteral("每月%1日 %2").arg(item.recurrence.monthDay).arg(recurrenceTimeText(item));
}
return QStringLiteral("一次");
}
QVector<ReminderItem> sortedReminders(QVector<ReminderItem> reminders) QVector<ReminderItem> sortedReminders(QVector<ReminderItem> reminders)
{ {
std::sort(reminders.begin(), reminders.end(), [](const ReminderItem &left, const ReminderItem &right) { std::sort(reminders.begin(), reminders.end(), [](const ReminderItem &left, const ReminderItem &right) {
+27
View File
@@ -19,6 +19,25 @@ enum class ReminderCommandType
Invalid, Invalid,
}; };
enum class ReminderRecurrenceType
{
None,
Daily,
Weekly,
Monthly,
};
struct ReminderRecurrence
{
ReminderRecurrenceType type = ReminderRecurrenceType::None;
int interval = 1;
int weekday = 0;
int monthDay = 0;
int hour = -1;
int minute = -1;
QDateTime lastTriggeredAt;
};
struct ReminderItem struct ReminderItem
{ {
QString id; QString id;
@@ -27,7 +46,9 @@ struct ReminderItem
QDateTime remindAt; QDateTime remindAt;
ReminderStatus status = ReminderStatus::Pending; ReminderStatus status = ReminderStatus::Pending;
QDateTime createdAt; QDateTime createdAt;
QDateTime finishedAt;
QString soundId; QString soundId;
ReminderRecurrence recurrence;
}; };
struct ReminderCommand struct ReminderCommand
@@ -38,9 +59,15 @@ struct ReminderCommand
QDateTime remindAt; QDateTime remindAt;
QString cancelQuery; QString cancelQuery;
QString errorMessage; QString errorMessage;
ReminderRecurrence recurrence;
}; };
QString reminderStatusToString(ReminderStatus status); QString reminderStatusToString(ReminderStatus status);
ReminderStatus reminderStatusFromString(const QString &status); ReminderStatus reminderStatusFromString(const QString &status);
QString reminderRecurrenceTypeToString(ReminderRecurrenceType type);
ReminderRecurrenceType reminderRecurrenceTypeFromString(const QString &type);
bool reminderIsRecurring(const ReminderItem &item);
bool reminderIsRecurring(const ReminderRecurrence &recurrence);
QString reminderDisplayTime(const QDateTime &dateTime); QString reminderDisplayTime(const QDateTime &dateTime);
QString reminderRecurrenceDisplayText(const ReminderItem &item);
QVector<ReminderItem> sortedReminders(QVector<ReminderItem> reminders); QVector<ReminderItem> sortedReminders(QVector<ReminderItem> reminders);
+4 -3
View File
@@ -62,14 +62,15 @@ void TrayController::show()
m_trayIcon.show(); m_trayIcon.show();
} }
void TrayController::showNotification(const QString &title, const QString &message) bool TrayController::showNotification(const QString &title, const QString &message)
{ {
if (!isAvailable() || !m_trayIcon.isVisible()) if (!isAvailable() || !m_trayIcon.isVisible() || !QSystemTrayIcon::supportsMessages())
{ {
return; return false;
} }
m_trayIcon.showMessage(title, message, QSystemTrayIcon::Information, 10000); m_trayIcon.showMessage(title, message, QSystemTrayIcon::Information, 10000);
return true;
} }
void TrayController::createMenu() void TrayController::createMenu()
+1 -1
View File
@@ -12,7 +12,7 @@ public:
bool isAvailable() const; bool isAvailable() const;
void show(); void show();
void showNotification(const QString &title, const QString &message); bool showNotification(const QString &title, const QString &message);
private: private:
void createMenu(); void createMenu();
+263 -14
View File
@@ -24,6 +24,7 @@
#include <QContextMenuEvent> #include <QContextMenuEvent>
#include <QCursor> #include <QCursor>
#include <QDialog> #include <QDialog>
#include <QFrame>
#include <QGuiApplication> #include <QGuiApplication>
#include <QHideEvent> #include <QHideEvent>
#include <QList> #include <QList>
@@ -32,6 +33,7 @@
#include <QPixmap> #include <QPixmap>
#include <QPointF> #include <QPointF>
#include <QPointer> #include <QPointer>
#include <QPushButton>
#include <QRandomGenerator> #include <QRandomGenerator>
#include <QScreen> #include <QScreen>
#include <QSet> #include <QSet>
@@ -333,6 +335,7 @@ void PetWindow::resumeAnimation()
void PetWindow::showBubbleMessage(const QString &message) void PetWindow::showBubbleMessage(const QString &message)
{ {
hideReminderActions();
m_chatBubble->showMessage(message, bubbleAnchorPosition()); m_chatBubble->showMessage(message, bubbleAnchorPosition());
} }
@@ -352,8 +355,47 @@ void PetWindow::openSettingsDialog()
[this](const QString &reminderId, QString *errorMessage) { [this](const QString &reminderId, QString *errorMessage) {
return m_reminderManager && m_reminderManager->cancelReminder(reminderId, errorMessage); return m_reminderManager && m_reminderManager->cancelReminder(reminderId, errorMessage);
}, },
[this](const QString &reminderId, const QString &title, const QDateTime &remindAt, const ReminderRecurrence &recurrence, ReminderItem *updatedItem, QString *errorMessage) {
if (!m_reminderManager)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("提醒功能初始化失败。");
}
return false;
}
if (!m_reminderManager->updateReminder(reminderId, title, remindAt, recurrence, errorMessage))
{
return false;
}
if (updatedItem != nullptr)
{
const QVector<ReminderItem> reminders = m_reminderManager->allReminders();
bool found = false;
for (const ReminderItem &item : reminders)
{
if (item.id == reminderId)
{
*updatedItem = item;
found = true;
break;
}
}
if (!found)
{
if (errorMessage != nullptr)
{
*errorMessage = QStringLiteral("提醒已更新,但没有找到更新后的记录。");
}
return false;
}
}
return true;
},
[this](QString *errorMessage) { [this](QString *errorMessage) {
return m_reminderManager && m_reminderManager->clearFinishedReminders(errorMessage); return m_reminderManager && m_reminderManager->pruneFinishedReminders(20, errorMessage);
}, },
[this](const QString &soundId, double volume) { [this](const QString &soundId, double volume) {
if (m_reminderSoundPlayer) if (m_reminderSoundPlayer)
@@ -402,6 +444,10 @@ void PetWindow::activateFromExternalInstance()
raise(); raise();
activateWindow(); activateWindow();
if (m_reminderManager)
{
m_reminderManager->checkDueRemindersNow();
}
updateBubblePosition(); updateBubblePosition();
} }
@@ -410,7 +456,7 @@ void PetWindow::setSettingsFallbackInContextMenuEnabled(bool enabled)
m_settingsFallbackInContextMenuEnabled = enabled; m_settingsFallbackInContextMenuEnabled = enabled;
} }
void PetWindow::setTrayNotificationCallback(std::function<void(const QString &, const QString &)> callback) void PetWindow::setTrayNotificationCallback(std::function<bool(const QString &, const QString &)> callback)
{ {
if (m_notificationDispatcher) if (m_notificationDispatcher)
{ {
@@ -547,37 +593,240 @@ bool PetWindow::handleReminderChatMessage(const QString &message)
} }
void PetWindow::handleTriggeredReminder(const ReminderItem &item) void PetWindow::handleTriggeredReminder(const ReminderItem &item)
{
playReminderSound();
if (shouldNotifyOnlyForReminder())
{
showReminderNotification(item);
return;
}
enqueueVisibleTriggeredReminder(item);
}
void PetWindow::playReminderSound()
{ {
if (m_appConfig.reminderSoundEnabled && m_reminderSoundPlayer) if (m_appConfig.reminderSoundEnabled && m_reminderSoundPlayer)
{ {
m_reminderSoundPlayer->play(m_appConfig.reminderSoundId, m_appConfig.reminderSoundVolume); m_reminderSoundPlayer->play(m_appConfig.reminderSoundId, m_appConfig.reminderSoundVolume);
} }
}
if (!isVisible()) void PetWindow::showReminderNotification(const ReminderItem &item)
{
if (m_notificationDispatcher)
{ {
if (m_notificationDispatcher) const bool shown = m_notificationDispatcher->showReminder(QStringLiteral("定时提醒"), QStringLiteral("到时间啦:%1").arg(item.title));
if (!shown)
{ {
m_notificationDispatcher->showReminder(QStringLiteral("定时提醒"), QStringLiteral("到时间啦:%1").arg(item.title)); Logger::warning(QStringLiteral("Reminder notification backend unavailable: id=%1").arg(item.id));
} }
return; return;
} }
if (m_dragging) Logger::warning(QStringLiteral("Reminder notification dispatcher is unavailable: id=%1").arg(item.id));
}
bool PetWindow::shouldNotifyOnlyForReminder() const
{
return !isVisible() || hasActiveAIRequest() || m_streamingChatActive;
}
void PetWindow::enqueueVisibleTriggeredReminder(const ReminderItem &item)
{
m_pendingVisibleTriggeredReminders.append(item);
showNextTriggeredReminder();
}
void PetWindow::showNextTriggeredReminder()
{
if (m_hasActiveTriggeredReminder || m_dragging || m_pendingVisibleTriggeredReminders.isEmpty())
{ {
m_deferredTriggeredReminders.append(item);
return; return;
} }
const ReminderItem item = m_pendingVisibleTriggeredReminders.takeFirst();
showTriggeredReminder(item); showTriggeredReminder(item);
} }
void PetWindow::finishActiveTriggeredReminder(bool hideBubble)
{
hideReminderActions();
m_hasActiveTriggeredReminder = false;
if (hideBubble && m_chatBubble)
{
m_chatBubble->hideBubble();
}
showNextTriggeredReminder();
}
void PetWindow::showTriggeredReminder(const ReminderItem &item) void PetWindow::showTriggeredReminder(const ReminderItem &item)
{ {
const QString reminderState = m_clips.contains(QStringLiteral("happy")) const QString reminderState = m_clips.contains(QStringLiteral("happy"))
? QStringLiteral("happy") ? QStringLiteral("happy")
: QStringLiteral("talk"); : QStringLiteral("talk");
playState(reminderState, false); playState(reminderState, false);
showBubbleMessage(QStringLiteral("到时间啦:%1").arg(item.title)); hideReminderActions();
m_chatBubble->showMessage(QStringLiteral("到时间啦:%1").arg(item.title), bubbleAnchorPosition(), 0);
showReminderActions(item);
}
void PetWindow::ensureReminderActionPanel()
{
if (m_reminderActionPanel)
{
return;
}
auto *panel = new QFrame();
panel->setObjectName(QStringLiteral("ReminderActionPanel"));
panel->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
panel->setAttribute(Qt::WA_TranslucentBackground);
panel->setAttribute(Qt::WA_ShowWithoutActivating);
panel->setStyleSheet(QStringLiteral(
"QFrame#ReminderActionPanel {"
"background: #ffffff;"
"border: 1px solid #c7cdd4;"
"border-radius: 8px;"
"}"
"QPushButton {"
"background: #f1f4f7;"
"border: 1px solid #aeb6bf;"
"border-radius: 6px;"
"padding: 7px 12px;"
"color: #202124;"
"}"
"QPushButton:hover {"
"background: #e4e9ee;"
"}"
"QPushButton:pressed {"
"background: #d5dce3;"
"}"));
auto *layout = new QVBoxLayout(panel);
layout->setContentsMargins(8, 8, 8, 8);
layout->setSpacing(8);
auto *dismissButton = new QPushButton(QStringLiteral("知道了"), panel);
auto *snoozeButton = new QPushButton(QStringLiteral("5分钟后再提醒"), panel);
layout->addWidget(dismissButton);
layout->addWidget(snoozeButton);
connect(dismissButton, &QPushButton::clicked, this, [this]() {
finishActiveTriggeredReminder(true);
});
connect(snoozeButton, &QPushButton::clicked, this, [this]() {
if (!m_hasActiveTriggeredReminder)
{
finishActiveTriggeredReminder(true);
return;
}
const ReminderItem item = m_activeTriggeredReminder;
hideReminderActions();
snoozeTriggeredReminder(item);
});
m_reminderActionPanel.reset(panel);
}
void PetWindow::showReminderActions(const ReminderItem &item)
{
m_activeTriggeredReminder = item;
m_hasActiveTriggeredReminder = true;
ensureReminderActionPanel();
if (!m_reminderActionPanel)
{
return;
}
m_reminderActionPanel->adjustSize();
updateReminderActionPosition();
m_reminderActionPanel->show();
m_reminderActionPanel->raise();
}
void PetWindow::hideReminderActions()
{
m_hasActiveTriggeredReminder = false;
if (m_reminderActionPanel)
{
m_reminderActionPanel->hide();
}
}
void PetWindow::updateReminderActionPosition()
{
if (!m_reminderActionPanel)
{
return;
}
m_reminderActionPanel->adjustSize();
const QSize panelSize = m_reminderActionPanel->sizeHint();
const QRect petGeometry = frameGeometry();
constexpr int PanelSideSpacing = 8;
QPoint position(
petGeometry.right() + PanelSideSpacing,
petGeometry.center().y() - panelSize.height() / 2);
if (QScreen *screen = screenForPopup(this))
{
const QRect availableGeometry = screen->availableGeometry();
const int maxX = qMax(availableGeometry.left(), availableGeometry.right() - panelSize.width());
const int maxY = qMax(availableGeometry.top(), availableGeometry.bottom() - panelSize.height());
if (position.x() > maxX)
{
position.setX(petGeometry.left() - panelSize.width() - PanelSideSpacing);
}
position.setX(qBound(
availableGeometry.left(),
position.x(),
maxX));
position.setY(qBound(
availableGeometry.top(),
position.y(),
maxY));
}
m_reminderActionPanel->move(position);
}
void PetWindow::snoozeTriggeredReminder(const ReminderItem &item)
{
m_hasActiveTriggeredReminder = false;
if (!m_reminderManager)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("提醒功能初始化失败。"));
QTimer::singleShot(1200, this, [this]() {
showNextTriggeredReminder();
});
return;
}
ReminderItem snoozedItem;
QString errorMessage;
if (!m_reminderManager->snoozeReminder(item, 5, &snoozedItem, &errorMessage))
{
playState(QStringLiteral("error"), false);
showBubbleMessage(errorMessage.isEmpty() ? QStringLiteral("创建稍后提醒失败。") : errorMessage);
QTimer::singleShot(1200, this, [this]() {
showNextTriggeredReminder();
});
return;
}
playState(QStringLiteral("talk"), false);
showBubbleMessage(QStringLiteral("已延后提醒:%1,时间:%2").arg(snoozedItem.title, reminderDisplayTime(snoozedItem.remindAt)));
QTimer::singleShot(1200, this, [this]() {
showNextTriggeredReminder();
});
} }
bool PetWindow::submitAiChatMessage(const QString &message) bool PetWindow::submitAiChatMessage(const QString &message)
@@ -620,6 +869,7 @@ bool PetWindow::submitAiChatMessage(const QString &message)
stopAnimationPrewarm(); stopAnimationPrewarm();
playState(QStringLiteral("think"), false); playState(QStringLiteral("think"), false);
hideReminderActions();
m_streamingAssistantText.clear(); m_streamingAssistantText.clear();
m_streamBubbleUpdateTimer.stop(); m_streamBubbleUpdateTimer.stop();
m_streamingChatActive = true; m_streamingChatActive = true;
@@ -844,6 +1094,7 @@ void PetWindow::flushStreamingBubble(bool finalUpdate)
return; return;
} }
hideReminderActions();
m_chatBubble->showMessage( m_chatBubble->showMessage(
m_streamingAssistantText, m_streamingAssistantText,
bubbleAnchorPosition(), bubbleAnchorPosition(),
@@ -888,6 +1139,7 @@ void PetWindow::hideEvent(QHideEvent *event)
{ {
m_chatBubble->hideBubble(); m_chatBubble->hideBubble();
} }
hideReminderActions();
if (m_chatInputDialog) if (m_chatInputDialog)
{ {
m_chatInputDialog->hide(); m_chatInputDialog->hide();
@@ -911,6 +1163,7 @@ void PetWindow::showEvent(QShowEvent *event)
if (m_reminderManager) if (m_reminderManager)
{ {
m_reminderManager->start(); m_reminderManager->start();
m_reminderManager->checkDueRemindersNow();
} }
scheduleAnimationPrewarm(); scheduleAnimationPrewarm();
} }
@@ -976,12 +1229,7 @@ void PetWindow::mouseReleaseEvent(QMouseEvent *event)
m_dragging = false; m_dragging = false;
playResolvedState(m_stateMachine.endDrag(), false); playResolvedState(m_stateMachine.endDrag(), false);
scheduleAnimationPrewarm(); scheduleAnimationPrewarm();
const QVector<ReminderItem> deferredReminders = m_deferredTriggeredReminders; showNextTriggeredReminder();
m_deferredTriggeredReminders.clear();
for (const ReminderItem &item : deferredReminders)
{
showTriggeredReminder(item);
}
event->accept(); event->accept();
return; return;
} }
@@ -1087,6 +1335,7 @@ void PetWindow::addStateTestActions(QMenu *menu)
void PetWindow::updateBubblePosition() void PetWindow::updateBubblePosition()
{ {
m_chatBubble->updateAnchorPosition(bubbleAnchorPosition()); m_chatBubble->updateAnchorPosition(bubbleAnchorPosition());
updateReminderActionPosition();
} }
QPoint PetWindow::bubbleAnchorPosition() const QPoint PetWindow::bubbleAnchorPosition() const
+16 -2
View File
@@ -45,7 +45,7 @@ public:
void openSettingsDialog(); void openSettingsDialog();
void activateFromExternalInstance(); void activateFromExternalInstance();
void setSettingsFallbackInContextMenuEnabled(bool enabled); void setSettingsFallbackInContextMenuEnabled(bool enabled);
void setTrayNotificationCallback(std::function<void(const QString &, const QString &)> callback); void setTrayNotificationCallback(std::function<bool(const QString &, const QString &)> callback);
void pauseAnimation(); void pauseAnimation();
void resumeAnimation(); void resumeAnimation();
void showBubbleMessage(const QString &message); void showBubbleMessage(const QString &message);
@@ -70,7 +70,18 @@ private:
bool submitAiChatMessage(const QString &message); bool submitAiChatMessage(const QString &message);
bool handleReminderChatMessage(const QString &message); bool handleReminderChatMessage(const QString &message);
void handleTriggeredReminder(const ReminderItem &item); void handleTriggeredReminder(const ReminderItem &item);
void playReminderSound();
void showReminderNotification(const ReminderItem &item);
bool shouldNotifyOnlyForReminder() const;
void enqueueVisibleTriggeredReminder(const ReminderItem &item);
void showNextTriggeredReminder();
void finishActiveTriggeredReminder(bool hideBubble);
void showTriggeredReminder(const ReminderItem &item); void showTriggeredReminder(const ReminderItem &item);
void ensureReminderActionPanel();
void showReminderActions(const ReminderItem &item);
void hideReminderActions();
void updateReminderActionPosition();
void snoozeTriggeredReminder(const ReminderItem &item);
void clearConversation(); void clearConversation();
void cancelActiveAIRequest(); void cancelActiveAIRequest();
void showConversationHistory(); void showConversationHistory();
@@ -121,6 +132,7 @@ private:
std::unique_ptr<NotificationDispatcher> m_notificationDispatcher; std::unique_ptr<NotificationDispatcher> m_notificationDispatcher;
std::unique_ptr<ReminderManager> m_reminderManager; std::unique_ptr<ReminderManager> m_reminderManager;
std::unique_ptr<ReminderSoundPlayer> m_reminderSoundPlayer; std::unique_ptr<ReminderSoundPlayer> m_reminderSoundPlayer;
std::unique_ptr<QWidget> m_reminderActionPanel;
PetView *m_petView; PetView *m_petView;
QTimer m_idleBehaviorTimer; QTimer m_idleBehaviorTimer;
QTimer m_behaviorReturnTimer; QTimer m_behaviorReturnTimer;
@@ -136,7 +148,8 @@ private:
QPoint m_dragOffset; QPoint m_dragOffset;
QString m_streamingAssistantText; QString m_streamingAssistantText;
QStringList m_animationPrewarmQueue; QStringList m_animationPrewarmQueue;
QVector<ReminderItem> m_deferredTriggeredReminders; QVector<ReminderItem> m_pendingVisibleTriggeredReminders;
ReminderItem m_activeTriggeredReminder;
qint64 m_clipAccessSerial = 0; qint64 m_clipAccessSerial = 0;
bool m_dragging; bool m_dragging;
bool m_alwaysOnTop; bool m_alwaysOnTop;
@@ -145,4 +158,5 @@ private:
bool m_streamingChatActive = false; bool m_streamingChatActive = false;
bool m_streamingTalkStarted = false; bool m_streamingTalkStarted = false;
bool m_settingsFallbackInContextMenuEnabled = true; bool m_settingsFallbackInContextMenuEnabled = true;
bool m_hasActiveTriggeredReminder = false;
}; };
+239 -13
View File
@@ -9,6 +9,7 @@
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QDateTimeEdit>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QFileDialog> #include <QFileDialog>
@@ -30,6 +31,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QStyle> #include <QStyle>
#include <QStringList> #include <QStringList>
#include <QTime>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <QtGlobal> #include <QtGlobal>
@@ -38,6 +40,8 @@
namespace namespace
{ {
constexpr int ReminderHistoryRetentionDays = 20;
QString normalizedProviderName(const QString &provider) QString normalizedProviderName(const QString &provider)
{ {
const QString normalized = provider.trimmed().toLower(); const QString normalized = provider.trimmed().toLower();
@@ -105,11 +109,22 @@ bool reminderMatchesStatusFilter(const ReminderItem &item, const QString &filter
return item.status == ReminderStatus::Pending; return item.status == ReminderStatus::Pending;
} }
bool hasFinishedReminders(const QVector<ReminderItem> &reminders) bool isFinishedReminder(const ReminderItem &item)
{ {
return item.status == ReminderStatus::Triggered || item.status == ReminderStatus::Canceled;
}
QDateTime reminderFinishedReferenceTime(const ReminderItem &item)
{
return item.finishedAt.isValid() ? item.finishedAt : item.remindAt;
}
bool hasPrunableFinishedReminders(const QVector<ReminderItem> &reminders, int retentionDays)
{
const QDateTime cutoff = QDateTime::currentDateTime().addDays(-qMax(0, retentionDays));
for (const ReminderItem &item : reminders) for (const ReminderItem &item : reminders)
{ {
if (item.status == ReminderStatus::Triggered || item.status == ReminderStatus::Canceled) if (isFinishedReminder(item) && reminderFinishedReferenceTime(item) < cutoff)
{ {
return true; return true;
} }
@@ -117,6 +132,158 @@ bool hasFinishedReminders(const QVector<ReminderItem> &reminders)
return false; return false;
} }
const ReminderItem *findReminderById(const QVector<ReminderItem> &reminders, const QString &id)
{
for (const ReminderItem &item : reminders)
{
if (item.id == id)
{
return &item;
}
}
return nullptr;
}
class ReminderEditDialog : public QDialog
{
public:
explicit ReminderEditDialog(const ReminderItem &item, QWidget *parent = nullptr)
: QDialog(parent)
, m_titleEdit(new QLineEdit(this))
, m_dateTimeEdit(new QDateTimeEdit(this))
, m_recurrenceComboBox(new QComboBox(this))
, m_weekdayComboBox(new QComboBox(this))
, m_monthDaySpinBox(new QSpinBox(this))
{
setWindowTitle(QStringLiteral("编辑提醒"));
setModal(true);
resize(420, 240);
m_titleEdit->setText(item.title);
const QDateTime minimumDateTime = QDateTime::currentDateTime().addSecs(1);
const QDateTime selectedDateTime = item.remindAt.isValid() && item.remindAt > minimumDateTime
? item.remindAt
: minimumDateTime;
m_dateTimeEdit->setCalendarPopup(true);
m_dateTimeEdit->setDisplayFormat(QStringLiteral("yyyy-MM-dd HH:mm"));
m_dateTimeEdit->setMinimumDateTime(minimumDateTime);
m_dateTimeEdit->setDateTime(selectedDateTime);
m_recurrenceComboBox->addItem(QStringLiteral("一次"), reminderRecurrenceTypeToString(ReminderRecurrenceType::None));
m_recurrenceComboBox->addItem(QStringLiteral("每天"), reminderRecurrenceTypeToString(ReminderRecurrenceType::Daily));
m_recurrenceComboBox->addItem(QStringLiteral("每周"), reminderRecurrenceTypeToString(ReminderRecurrenceType::Weekly));
m_recurrenceComboBox->addItem(QStringLiteral("每月"), reminderRecurrenceTypeToString(ReminderRecurrenceType::Monthly));
const int recurrenceIndex = m_recurrenceComboBox->findData(reminderRecurrenceTypeToString(item.recurrence.type));
m_recurrenceComboBox->setCurrentIndex(recurrenceIndex >= 0 ? recurrenceIndex : 0);
m_weekdayComboBox->addItem(QStringLiteral("周一"), 1);
m_weekdayComboBox->addItem(QStringLiteral("周二"), 2);
m_weekdayComboBox->addItem(QStringLiteral("周三"), 3);
m_weekdayComboBox->addItem(QStringLiteral("周四"), 4);
m_weekdayComboBox->addItem(QStringLiteral("周五"), 5);
m_weekdayComboBox->addItem(QStringLiteral("周六"), 6);
m_weekdayComboBox->addItem(QStringLiteral("周日"), 7);
const int weekday = item.recurrence.weekday >= 1 && item.recurrence.weekday <= 7
? item.recurrence.weekday
: selectedDateTime.date().dayOfWeek();
const int weekdayIndex = m_weekdayComboBox->findData(weekday);
m_weekdayComboBox->setCurrentIndex(weekdayIndex >= 0 ? weekdayIndex : 0);
m_monthDaySpinBox->setRange(1, 31);
m_monthDaySpinBox->setValue(item.recurrence.monthDay >= 1 && item.recurrence.monthDay <= 31
? item.recurrence.monthDay
: selectedDateTime.date().day());
auto *formLayout = new QFormLayout();
formLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
formLayout->addRow(QStringLiteral("标题"), m_titleEdit);
formLayout->addRow(QStringLiteral("下一次时间"), m_dateTimeEdit);
formLayout->addRow(QStringLiteral("重复"), m_recurrenceComboBox);
formLayout->addRow(QStringLiteral("每周"), m_weekdayComboBox);
formLayout->addRow(QStringLiteral("每月日期"), m_monthDaySpinBox);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, [this]() {
accept();
});
connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() {
reject();
});
connect(m_recurrenceComboBox, &QComboBox::currentIndexChanged, this, [this]() {
updateRecurrenceControls();
});
auto *layout = new QVBoxLayout(this);
layout->addLayout(formLayout);
layout->addWidget(buttonBox);
updateRecurrenceControls();
}
QString title() const
{
return m_titleEdit->text().trimmed();
}
QDateTime remindAt() const
{
return m_dateTimeEdit->dateTime();
}
ReminderRecurrence recurrence() const
{
ReminderRecurrence recurrence;
recurrence.type = reminderRecurrenceTypeFromString(m_recurrenceComboBox->currentData().toString());
if (recurrence.type == ReminderRecurrenceType::None)
{
return {};
}
const QTime time = m_dateTimeEdit->dateTime().time();
recurrence.interval = 1;
recurrence.hour = time.hour();
recurrence.minute = time.minute();
if (recurrence.type == ReminderRecurrenceType::Weekly)
{
recurrence.weekday = m_weekdayComboBox->currentData().toInt();
}
else if (recurrence.type == ReminderRecurrenceType::Monthly)
{
recurrence.monthDay = m_monthDaySpinBox->value();
}
return recurrence;
}
protected:
void accept() override
{
const ReminderRecurrenceType type = reminderRecurrenceTypeFromString(m_recurrenceComboBox->currentData().toString());
if (type == ReminderRecurrenceType::None && m_dateTimeEdit->dateTime() <= QDateTime::currentDateTime())
{
QMessageBox::warning(this, QStringLiteral("编辑提醒"), QStringLiteral("提醒时间必须晚于当前时间。"));
return;
}
QDialog::accept();
}
private:
void updateRecurrenceControls()
{
const ReminderRecurrenceType type = reminderRecurrenceTypeFromString(m_recurrenceComboBox->currentData().toString());
m_weekdayComboBox->setEnabled(type == ReminderRecurrenceType::Weekly);
m_monthDaySpinBox->setEnabled(type == ReminderRecurrenceType::Monthly);
}
QLineEdit *m_titleEdit = nullptr;
QDateTimeEdit *m_dateTimeEdit = nullptr;
QComboBox *m_recurrenceComboBox = nullptr;
QComboBox *m_weekdayComboBox = nullptr;
QSpinBox *m_monthDaySpinBox = nullptr;
};
} }
SettingsDialog::SettingsDialog( SettingsDialog::SettingsDialog(
@@ -126,6 +293,7 @@ SettingsDialog::SettingsDialog(
std::function<bool()> aiTestBlocked, std::function<bool()> aiTestBlocked,
std::function<void()> clearConversationHistoryCallback, std::function<void()> clearConversationHistoryCallback,
std::function<bool(const QString &, QString *)> cancelReminderCallback, std::function<bool(const QString &, QString *)> cancelReminderCallback,
std::function<bool(const QString &, const QString &, const QDateTime &, const ReminderRecurrence &, ReminderItem *, QString *)> updateReminderCallback,
std::function<bool(QString *)> clearFinishedRemindersCallback, std::function<bool(QString *)> clearFinishedRemindersCallback,
std::function<void(const QString &, double)> playReminderSoundCallback, std::function<void(const QString &, double)> playReminderSoundCallback,
QWidget *parent) QWidget *parent)
@@ -159,7 +327,8 @@ SettingsDialog::SettingsDialog(
, m_reminderStatusFilterComboBox(new QComboBox(this)) , m_reminderStatusFilterComboBox(new QComboBox(this))
, m_reminderListWidget(new QListWidget(this)) , m_reminderListWidget(new QListWidget(this))
, m_cancelReminderButton(new QPushButton(QStringLiteral("取消选中提醒"), this)) , m_cancelReminderButton(new QPushButton(QStringLiteral("取消选中提醒"), this))
, m_clearFinishedRemindersButton(new QPushButton(QStringLiteral("清理历史"), this)) , m_editReminderButton(new QPushButton(QStringLiteral("编辑选中提醒"), this))
, m_clearFinishedRemindersButton(new QPushButton(QStringLiteral("清理20天前历史"), this))
, m_reminderStatusLabel(new QLabel(this)) , m_reminderStatusLabel(new QLabel(this))
, m_reminderSoundEnabledCheckBox(new QCheckBox(QStringLiteral("启用提醒音效"), this)) , m_reminderSoundEnabledCheckBox(new QCheckBox(QStringLiteral("启用提醒音效"), this))
, m_reminderSoundVolumeSpinBox(new QSpinBox(this)) , m_reminderSoundVolumeSpinBox(new QSpinBox(this))
@@ -174,6 +343,7 @@ SettingsDialog::SettingsDialog(
, m_aiTestBlocked(std::move(aiTestBlocked)) , m_aiTestBlocked(std::move(aiTestBlocked))
, m_clearConversationHistory(std::move(clearConversationHistoryCallback)) , m_clearConversationHistory(std::move(clearConversationHistoryCallback))
, m_cancelReminder(std::move(cancelReminderCallback)) , m_cancelReminder(std::move(cancelReminderCallback))
, m_updateReminder(std::move(updateReminderCallback))
, m_clearFinishedReminders(std::move(clearFinishedRemindersCallback)) , m_clearFinishedReminders(std::move(clearFinishedRemindersCallback))
, m_playReminderSound(std::move(playReminderSoundCallback)) , m_playReminderSound(std::move(playReminderSoundCallback))
{ {
@@ -378,7 +548,7 @@ SettingsDialog::SettingsDialog(
auto *reminderTitleLabel = new QLabel(QStringLiteral("提醒"), this); auto *reminderTitleLabel = new QLabel(QStringLiteral("提醒"), this);
reminderTitleLabel->setObjectName(QStringLiteral("PageTitle")); reminderTitleLabel->setObjectName(QStringLiteral("PageTitle"));
auto *reminderHintLabel = new QLabel(QStringLiteral("这里显示通过聊天创建的待触发提醒。内置提醒音效不可删除,导入音效仅支持 PCM wav。"), this); auto *reminderHintLabel = new QLabel(QStringLiteral("这里显示通过聊天创建的提醒,重复提醒会展示下一次触发时间。内置提醒音效不可删除,导入音效仅支持 PCM wav。"), this);
reminderHintLabel->setObjectName(QStringLiteral("HintLabel")); reminderHintLabel->setObjectName(QStringLiteral("HintLabel"));
reminderHintLabel->setWordWrap(true); reminderHintLabel->setWordWrap(true);
@@ -392,6 +562,7 @@ SettingsDialog::SettingsDialog(
reminderListLayout->addWidget(m_reminderListWidget); reminderListLayout->addWidget(m_reminderListWidget);
auto *reminderActionLayout = new QHBoxLayout(); auto *reminderActionLayout = new QHBoxLayout();
reminderActionLayout->addWidget(m_cancelReminderButton); reminderActionLayout->addWidget(m_cancelReminderButton);
reminderActionLayout->addWidget(m_editReminderButton);
reminderActionLayout->addWidget(m_clearFinishedRemindersButton); reminderActionLayout->addWidget(m_clearFinishedRemindersButton);
reminderActionLayout->addWidget(m_reminderStatusLabel, 1); reminderActionLayout->addWidget(m_reminderStatusLabel, 1);
reminderListLayout->addLayout(reminderActionLayout); reminderListLayout->addLayout(reminderActionLayout);
@@ -572,6 +743,9 @@ SettingsDialog::SettingsDialog(
connect(m_cancelReminderButton, &QPushButton::clicked, this, [this]() { connect(m_cancelReminderButton, &QPushButton::clicked, this, [this]() {
cancelSelectedReminder(); cancelSelectedReminder();
}); });
connect(m_editReminderButton, &QPushButton::clicked, this, [this]() {
editSelectedReminder();
});
connect(m_clearFinishedRemindersButton, &QPushButton::clicked, this, [this]() { connect(m_clearFinishedRemindersButton, &QPushButton::clicked, this, [this]() {
clearFinishedReminders(); clearFinishedReminders();
}); });
@@ -967,8 +1141,8 @@ void SettingsDialog::reloadReminderList()
} }
auto *listItem = new QListWidgetItem( auto *listItem = new QListWidgetItem(
QStringLiteral("%1 %2 %3 %4") QStringLiteral("%1 %2 %3 %4 %5")
.arg(reminderDisplayTime(item.remindAt), reminderStatusDisplayText(item.status), item.title, item.originalText), .arg(reminderDisplayTime(item.remindAt), reminderStatusDisplayText(item.status), reminderRecurrenceDisplayText(item), item.title, item.originalText),
m_reminderListWidget); m_reminderListWidget);
listItem->setData(Qt::UserRole, item.id); listItem->setData(Qt::UserRole, item.id);
listItem->setData(Qt::UserRole + 1, reminderStatusToString(item.status)); listItem->setData(Qt::UserRole + 1, reminderStatusToString(item.status));
@@ -1042,7 +1216,8 @@ void SettingsDialog::updateReminderActionButtons()
const bool selectedPending = item != nullptr const bool selectedPending = item != nullptr
&& item->data(Qt::UserRole + 1).toString() == QStringLiteral("pending"); && item->data(Qt::UserRole + 1).toString() == QStringLiteral("pending");
m_cancelReminderButton->setEnabled(selectedPending); m_cancelReminderButton->setEnabled(selectedPending);
m_clearFinishedRemindersButton->setEnabled(hasFinishedReminders(m_reminders)); m_editReminderButton->setEnabled(selectedPending);
m_clearFinishedRemindersButton->setEnabled(hasPrunableFinishedReminders(m_reminders, ReminderHistoryRetentionDays));
} }
void SettingsDialog::cancelSelectedReminder() void SettingsDialog::cancelSelectedReminder()
@@ -1078,6 +1253,7 @@ void SettingsDialog::cancelSelectedReminder()
if (reminder.id == reminderId) if (reminder.id == reminderId)
{ {
reminder.status = ReminderStatus::Canceled; reminder.status = ReminderStatus::Canceled;
reminder.finishedAt = QDateTime::currentDateTime();
break; break;
} }
} }
@@ -1085,18 +1261,68 @@ void SettingsDialog::cancelSelectedReminder()
m_reminderStatusLabel->setText(QStringLiteral("已取消提醒:%1").arg(reminderId)); m_reminderStatusLabel->setText(QStringLiteral("已取消提醒:%1").arg(reminderId));
} }
void SettingsDialog::editSelectedReminder()
{
QListWidgetItem *item = m_reminderListWidget->currentItem();
if (item == nullptr)
{
return;
}
const QString reminderId = item->data(Qt::UserRole).toString();
const ReminderItem *reminder = findReminderById(m_reminders, reminderId);
if (reminder == nullptr || reminder->status != ReminderStatus::Pending)
{
QMessageBox::information(
this,
QStringLiteral("编辑提醒"),
QStringLiteral("只能编辑待提醒事项。"));
return;
}
ReminderEditDialog dialog(*reminder, this);
if (dialog.exec() != QDialog::Accepted)
{
return;
}
ReminderItem updatedReminder;
QString errorMessage;
if (!m_updateReminder
|| !m_updateReminder(reminderId, dialog.title(), dialog.remindAt(), dialog.recurrence(), &updatedReminder, &errorMessage))
{
QMessageBox::warning(
this,
QStringLiteral("编辑提醒失败"),
errorMessage.isEmpty() ? QStringLiteral("编辑提醒失败。") : errorMessage);
return;
}
for (ReminderItem &existingReminder : m_reminders)
{
if (existingReminder.id == reminderId)
{
existingReminder = updatedReminder;
break;
}
}
reloadReminderList();
m_reminderStatusLabel->setText(QStringLiteral("已更新提醒:%1").arg(updatedReminder.id));
}
void SettingsDialog::clearFinishedReminders() void SettingsDialog::clearFinishedReminders()
{ {
if (!hasFinishedReminders(m_reminders)) if (!hasPrunableFinishedReminders(m_reminders, ReminderHistoryRetentionDays))
{ {
m_reminderStatusLabel->setText(QStringLiteral("没有可清理的历史提醒。")); m_reminderStatusLabel->setText(QStringLiteral("没有可清理的20天前历史提醒。"));
return; return;
} }
const QMessageBox::StandardButton result = QMessageBox::warning( const QMessageBox::StandardButton result = QMessageBox::warning(
this, this,
QStringLiteral("清理提醒历史"), QStringLiteral("清理提醒历史"),
QStringLiteral("确定要清理所有已触发和已取消提醒记录吗?\n\n待提醒事项不会被删除。"), QStringLiteral("确定要清理20天前的已触发和已取消提醒记录吗?\n\n最近20天历史会保留,待提醒事项不会被删除。"),
QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel); QMessageBox::Cancel);
if (result != QMessageBox::Yes) if (result != QMessageBox::Yes)
@@ -1116,15 +1342,15 @@ void SettingsDialog::clearFinishedReminders()
for (int index = m_reminders.size() - 1; index >= 0; --index) for (int index = m_reminders.size() - 1; index >= 0; --index)
{ {
const ReminderStatus status = m_reminders.at(index).status; if (isFinishedReminder(m_reminders.at(index))
if (status == ReminderStatus::Triggered || status == ReminderStatus::Canceled) && reminderFinishedReferenceTime(m_reminders.at(index)) < QDateTime::currentDateTime().addDays(-ReminderHistoryRetentionDays))
{ {
m_reminders.removeAt(index); m_reminders.removeAt(index);
} }
} }
reloadReminderList(); reloadReminderList();
m_reminderStatusLabel->setText(QStringLiteral("已清理提醒历史。")); m_reminderStatusLabel->setText(QStringLiteral("已清理20天前提醒历史。"));
} }
void SettingsDialog::importReminderSound() void SettingsDialog::importReminderSound()
+4
View File
@@ -30,6 +30,7 @@ public:
std::function<bool()> aiTestBlocked, std::function<bool()> aiTestBlocked,
std::function<void()> clearConversationHistoryCallback, std::function<void()> clearConversationHistoryCallback,
std::function<bool(const QString &, QString *)> cancelReminderCallback, std::function<bool(const QString &, QString *)> cancelReminderCallback,
std::function<bool(const QString &, const QString &, const QDateTime &, const ReminderRecurrence &, ReminderItem *, QString *)> updateReminderCallback,
std::function<bool(QString *)> clearFinishedRemindersCallback, std::function<bool(QString *)> clearFinishedRemindersCallback,
std::function<void(const QString &, double)> playReminderSoundCallback, std::function<void(const QString &, double)> playReminderSoundCallback,
QWidget *parent = nullptr); QWidget *parent = nullptr);
@@ -61,6 +62,7 @@ private:
void updateReminderSoundButtons(); void updateReminderSoundButtons();
void updateReminderActionButtons(); void updateReminderActionButtons();
void cancelSelectedReminder(); void cancelSelectedReminder();
void editSelectedReminder();
void clearFinishedReminders(); void clearFinishedReminders();
void importReminderSound(); void importReminderSound();
void deleteSelectedReminderSound(); void deleteSelectedReminderSound();
@@ -97,6 +99,7 @@ private:
QComboBox *m_reminderStatusFilterComboBox = nullptr; QComboBox *m_reminderStatusFilterComboBox = nullptr;
QListWidget *m_reminderListWidget = nullptr; QListWidget *m_reminderListWidget = nullptr;
QPushButton *m_cancelReminderButton = nullptr; QPushButton *m_cancelReminderButton = nullptr;
QPushButton *m_editReminderButton = nullptr;
QPushButton *m_clearFinishedRemindersButton = nullptr; QPushButton *m_clearFinishedRemindersButton = nullptr;
QLabel *m_reminderStatusLabel = nullptr; QLabel *m_reminderStatusLabel = nullptr;
QCheckBox *m_reminderSoundEnabledCheckBox = nullptr; QCheckBox *m_reminderSoundEnabledCheckBox = nullptr;
@@ -114,6 +117,7 @@ private:
std::function<bool()> m_aiTestBlocked; std::function<bool()> m_aiTestBlocked;
std::function<void()> m_clearConversationHistory; std::function<void()> m_clearConversationHistory;
std::function<bool(const QString &, QString *)> m_cancelReminder; std::function<bool(const QString &, QString *)> m_cancelReminder;
std::function<bool(const QString &, const QString &, const QDateTime &, const ReminderRecurrence &, ReminderItem *, QString *)> m_updateReminder;
std::function<bool(QString *)> m_clearFinishedReminders; std::function<bool(QString *)> m_clearFinishedReminders;
std::function<void(const QString &, double)> m_playReminderSound; std::function<void(const QString &, double)> m_playReminderSound;
std::unique_ptr<LLMProvider> m_testProvider; std::unique_ptr<LLMProvider> m_testProvider;