完善定时提醒稳定性与管理能力
This commit is contained in:
@@ -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 第三步:天气查询
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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)),
|
||||||
|
|||||||
@@ -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,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
@@ -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 ¤tDate)
|
ReminderDateResolution resolveReminderDate(const QString &text, const QDate ¤tDate)
|
||||||
{
|
{
|
||||||
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)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user