完善定时提醒稳定性与管理能力
This commit is contained in:
@@ -35,8 +35,8 @@ QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物项目
|
||||
- Google Gemini 原生聊天请求
|
||||
- 角色文件夹导入和角色切换
|
||||
- 删除用户导入角色
|
||||
- 本地一次性提醒:聊天创建、查询、取消,重启后 pending 提醒不丢
|
||||
- 提醒到点气泡提示、拖动后延迟提示和隐藏时托盘通知
|
||||
- 本地一次性和重复提醒:聊天创建、查询、取消,重启后 pending 提醒不丢
|
||||
- 提醒到点气泡提示、稍后提醒、拖动后延迟提示和隐藏时托盘通知
|
||||
- 提醒音效切换、试听、用户 wav 导入和删除
|
||||
- Windows 发布打包脚本和 Inno Setup 安装器脚本
|
||||
- Windows GUI 子系统,Release exe 双击不弹控制台窗口
|
||||
@@ -151,7 +151,7 @@ resources/characters/shiroko/
|
||||
|
||||
## 定时提醒和音效
|
||||
|
||||
当前支持通过聊天输入创建一次性本地提醒,例如:
|
||||
当前支持通过聊天输入创建一次性和重复本地提醒,例如:
|
||||
|
||||
```text
|
||||
10分钟后提醒我喝水
|
||||
@@ -161,6 +161,14 @@ resources/characters/shiroko/
|
||||
后天9点提醒我开会
|
||||
6月3日9点提醒我提交
|
||||
下周一上午10点提醒我周会
|
||||
每天9点提醒我打卡
|
||||
每天提醒我9点打卡
|
||||
每日晚上8点提醒我吃药
|
||||
每周一上午10点提醒我周会
|
||||
每周一提醒我上午10点周会
|
||||
每星期五下午3点提醒我提交周报
|
||||
每月3号9点提醒我交报告
|
||||
每月3号提醒我9点交报告
|
||||
提醒列表
|
||||
取消喝水提醒
|
||||
```
|
||||
@@ -171,6 +179,12 @@ resources/characters/shiroko/
|
||||
QStandardPaths::AppConfigLocation/reminders.json
|
||||
```
|
||||
|
||||
提醒数据使用原子写入,写入失败时不会触发到点 UI,也不会覆盖旧的有效提醒文件。已触发和已取消记录会写入 `finishedAt`;旧版数据没有该字段时按 `remindAt` 兼容读取。
|
||||
|
||||
提醒调度保留最近提醒的精确 timer,同时每 60 秒做一次兜底扫描;程序显示、外部激活或系统睡眠唤醒后,都会重新检查已到期 pending 提醒。
|
||||
|
||||
设置页支持编辑 pending 提醒的标题、下一次时间和重复规则;已触发/已取消历史只读。历史记录默认只保留最近 20 天,设置页“清理20天前历史”只删除超过 20 天的已触发/已取消记录,不影响 pending。
|
||||
|
||||
提醒文件损坏时会备份为:
|
||||
|
||||
```text
|
||||
@@ -202,8 +216,15 @@ QStandardPaths::AppDataLocation/sounds/reminders/
|
||||
|
||||
- 桌宠可见时显示气泡,不发系统通知
|
||||
- 桌宠隐藏时发 Windows 托盘通知,不在下次显示时补气泡
|
||||
- AI 正在请求或流式回复时,按隐藏场景处理:播放音效并发 Windows 托盘通知,不显示气泡
|
||||
- 托盘或系统通知后端不可用时只记录日志,不补气泡
|
||||
- 用户拖动中不打断 `drag`,拖动结束后显示气泡
|
||||
- 重复提醒尚未支持,包含“每天 / 每周 / 每月”等语义时会提示暂不支持
|
||||
- 多条提醒同时触发时,可见状态下会按队列逐条展示
|
||||
- 桌宠可见触发时显示 `知道了` 和 `5分钟后再提醒`
|
||||
- `5分钟后再提醒` 会创建一条新的一次性提醒,不影响原重复规则
|
||||
- 重复提醒支持 `每天 / 每周 / 每月`;`工作日 / 每两天 / 每月最后一天 / 自定义间隔 / 农历` 等复杂规则暂不支持
|
||||
- 每月 31 号这类规则会跳过不存在该日期的月份,寻找下一个有效月份
|
||||
- 用户音效删除仅允许删除用户音效目录内的安全 sound id,内置音效和非法路径不会被删除
|
||||
|
||||
## 配置和日志
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
- AI 请求取消和对话清空
|
||||
- 角色文件夹导入和角色切换
|
||||
- 删除用户导入角色
|
||||
- 本地一次性提醒、提醒列表、取消提醒和到点通知
|
||||
- 本地一次性/重复提醒、提醒列表、取消提醒和到点通知
|
||||
- 内置/用户提醒音效切换、导入、删除和试听
|
||||
- Windows 打包脚本和 Inno Setup 安装器脚本
|
||||
- Release exe 双击不弹控制台窗口
|
||||
@@ -366,7 +366,7 @@ CommandDispatcher::dispatch(userText)
|
||||
|
||||
## 5.1 功能定位
|
||||
|
||||
实现本地一次性提醒功能。
|
||||
实现本地一次性和基础重复提醒功能。
|
||||
|
||||
用户可以输入:
|
||||
|
||||
@@ -386,12 +386,19 @@ CommandDispatcher::dispatch(userText)
|
||||
已新增 src/reminder/ 模块
|
||||
已支持一次性提醒解析、JSON 持久化、启动后加载、到点触发和状态标记
|
||||
已支持聊天创建 / 查询 / 取消提醒
|
||||
已支持设置页按状态查看提醒、取消 pending 提醒、清理已触发/已取消历史
|
||||
已支持设置页按状态查看提醒、取消 pending 提醒、编辑 pending 提醒、清理 20 天前已触发/已取消历史
|
||||
已支持 reminder_default / reminder_soft 内置音效
|
||||
已支持用户 wav 音效导入、删除、切换和试听
|
||||
已限制用户音效删除路径,只允许删除用户音效目录内的安全 sound id
|
||||
提醒触发时使用当前设置页选择的全局音效,ReminderItem.soundId 仅保留为历史兼容字段
|
||||
已接入 Qt Multimedia / QSoundEffect 播放提醒音效
|
||||
已预留 NotificationDispatcher,当前 Windows 仍由托盘通知承接
|
||||
已支持每天 / 每周 / 每月重复提醒
|
||||
已支持提醒触发后的“知道了”和“5分钟后再提醒”
|
||||
已支持提醒数据原子保存
|
||||
已支持多提醒可见队列,避免同时触发时互相覆盖
|
||||
已支持 60 秒兜底扫描,覆盖睡眠唤醒、系统时间变化和长间隔 timer 延迟场景
|
||||
通知后端不可用时会记录日志,不补气泡
|
||||
```
|
||||
|
||||
## 5.2 第一版范围
|
||||
@@ -400,6 +407,7 @@ CommandDispatcher::dispatch(userText)
|
||||
|
||||
```text
|
||||
一次性提醒
|
||||
每天 / 每周 / 每月重复提醒
|
||||
本地保存
|
||||
程序重启后提醒不丢
|
||||
到点后触发气泡和托盘通知
|
||||
@@ -411,7 +419,8 @@ CommandDispatcher::dispatch(userText)
|
||||
暂不做:
|
||||
|
||||
```text
|
||||
重复提醒
|
||||
工作日提醒
|
||||
自定义间隔重复提醒
|
||||
农历提醒
|
||||
复杂日历
|
||||
跨设备同步
|
||||
@@ -449,7 +458,20 @@ struct ReminderItem
|
||||
QDateTime remindAt;
|
||||
ReminderStatus status = ReminderStatus::Pending;
|
||||
QDateTime createdAt;
|
||||
QDateTime finishedAt; // triggered/canceled 记录的完成时间;旧数据缺失时按 remindAt 兼容
|
||||
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",
|
||||
"status": "pending",
|
||||
"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
|
||||
@@ -511,9 +547,17 @@ reminders.broken.yyyyMMdd-HHmmss.json
|
||||
1小时后
|
||||
两小时后
|
||||
2小时后
|
||||
每天9点
|
||||
每天提醒我9点打卡
|
||||
每日晚上8点
|
||||
每周一上午10点
|
||||
每周一提醒我上午10点周会
|
||||
每星期五下午3点
|
||||
每月3号9点
|
||||
每月3号提醒我9点交报告
|
||||
```
|
||||
|
||||
如果规则解析失败,后续可以再接 AI 解析。包含“每天 / 每周 / 每月 / 工作日 / 重复”等语义时,当前只返回“重复提醒尚未支持”,不创建一次性提醒。
|
||||
如果规则解析失败,后续可以再接 AI 解析。当前重复提醒只支持每天 / 每周 / 每月。包含“工作日 / 每两天 / 每月最后一天 / 每季度 / 农历 / 自定义复杂规则”等语义时,返回明确暂不支持提示,不创建一次性提醒。
|
||||
|
||||
## 5.7 AI 辅助解析的设计边界
|
||||
|
||||
@@ -558,7 +602,12 @@ userText
|
||||
```text
|
||||
桌宠可见:播放当前全局音效,显示 ChatBubble + 切 happy,无 happy 时回退 talk,不发 Windows 通知
|
||||
桌宠隐藏:播放当前全局音效,触发 Windows 托盘通知,不在下次显示时补气泡
|
||||
AI 正在请求或流式回复:按隐藏场景处理,播放当前全局音效并发 Windows 通知,不显示气泡,不进入补气泡队列
|
||||
Windows 托盘通知后端不可用:记录日志,不补气泡,不进入可见队列
|
||||
用户拖动中:播放当前全局音效,不打断 drag,拖动结束后显示气泡,不发 Windows 通知
|
||||
多条提醒同时触发:可见状态下进入队列逐条展示,避免后一条覆盖前一条
|
||||
桌宠可见触发时显示轻量操作区:“知道了”关闭当前提示,“5分钟后再提醒”固定创建新的 5 分钟一次性提醒
|
||||
重复提醒触发后先推进下一次 remindAt 并保存;稍后提醒不会影响原重复规则
|
||||
```
|
||||
|
||||
提醒文案:
|
||||
@@ -1020,8 +1069,8 @@ CustomSearchProvider
|
||||
## 9.2 第二步:定时提醒
|
||||
|
||||
```text
|
||||
当前已落地一次性提醒、提醒列表、取消提醒、到点气泡/托盘通知和提醒音效管理。
|
||||
后续可继续补确认/稍后提醒、重复提醒和跨平台通知实现。
|
||||
当前已落地一次性提醒、每天/每周/每月重复提醒、提醒列表、取消提醒、编辑 pending 提醒、20 天历史保留、到点气泡/托盘通知、稍后提醒和提醒音效管理。
|
||||
后续可继续补工作日/自定义间隔、跨平台通知实现和更复杂的提醒管理能力。
|
||||
```
|
||||
|
||||
## 9.3 第三步:天气查询
|
||||
|
||||
@@ -77,7 +77,7 @@ int main(int argc, char *argv[])
|
||||
TrayController trayController(&window);
|
||||
window.setSettingsFallbackInContextMenuEnabled(!trayController.isAvailable());
|
||||
window.setTrayNotificationCallback([&trayController](const QString &title, const QString &message) {
|
||||
trayController.showNotification(title, message);
|
||||
return trayController.showNotification(title, message);
|
||||
});
|
||||
trayController.show();
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ void NotificationDispatcher::setShowCallback(ShowCallback 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)
|
||||
{
|
||||
m_showCallback(title, message);
|
||||
return m_showCallback(title, message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
class NotificationDispatcher
|
||||
{
|
||||
public:
|
||||
using ShowCallback = std::function<void(const QString &, const QString &)>;
|
||||
using ShowCallback = std::function<bool(const QString &, const QString &)>;
|
||||
|
||||
void setShowCallback(ShowCallback callback);
|
||||
void showReminder(const QString &title, const QString &message) const;
|
||||
bool showReminder(const QString &title, const QString &message) const;
|
||||
|
||||
private:
|
||||
ShowCallback m_showCallback;
|
||||
|
||||
@@ -11,11 +11,21 @@ ReminderCommandResult ReminderCommandHandler::handle(const QString &text, Remind
|
||||
{
|
||||
ReminderItem item;
|
||||
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, {}};
|
||||
}
|
||||
|
||||
if (reminderIsRecurring(item))
|
||||
{
|
||||
return {
|
||||
true,
|
||||
QStringLiteral("已设置重复提醒:%1,规则:%2,下一次:%3")
|
||||
.arg(item.title, reminderRecurrenceDisplayText(item), reminderDisplayTime(item.remindAt)),
|
||||
item,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
true,
|
||||
QStringLiteral("已设置提醒:%1,时间:%2").arg(item.title, reminderDisplayTime(item.remindAt)),
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
#include "../util/Logger.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QRandomGenerator>
|
||||
#include <QStringList>
|
||||
#include <QTime>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <utility>
|
||||
@@ -13,12 +15,24 @@ namespace
|
||||
{
|
||||
constexpr qint64 MinimumTimerIntervalMs = 1000;
|
||||
constexpr qint64 MaximumTimerIntervalMs = 24 * 60 * 60 * 1000;
|
||||
constexpr int GuardTimerIntervalMs = 60 * 1000;
|
||||
constexpr int DefaultHistoryRetentionDays = 20;
|
||||
|
||||
bool isPending(const ReminderItem &item)
|
||||
{
|
||||
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)
|
||||
{
|
||||
const QString normalizedQuery = query.trimmed();
|
||||
@@ -31,6 +45,159 @@ bool textMatchesReminder(const ReminderItem &item, const QString &query)
|
||||
|| item.title.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()
|
||||
@@ -38,19 +205,26 @@ ReminderManager::ReminderManager()
|
||||
QObject::connect(&m_timer, &QTimer::timeout, [this]() {
|
||||
processDueReminders();
|
||||
});
|
||||
QObject::connect(&m_guardTimer, &QTimer::timeout, [this]() {
|
||||
checkDueRemindersNow();
|
||||
});
|
||||
m_timer.setSingleShot(true);
|
||||
load();
|
||||
}
|
||||
|
||||
void ReminderManager::start()
|
||||
{
|
||||
if (m_started)
|
||||
if (!m_started)
|
||||
{
|
||||
return;
|
||||
m_started = true;
|
||||
}
|
||||
|
||||
m_started = true;
|
||||
processDueReminders();
|
||||
if (!m_guardTimer.isActive())
|
||||
{
|
||||
m_guardTimer.start(GuardTimerIntervalMs);
|
||||
}
|
||||
|
||||
checkDueRemindersNow();
|
||||
}
|
||||
|
||||
void ReminderManager::setTriggeredCallback(TriggeredCallback callback)
|
||||
@@ -81,12 +255,38 @@ ReminderCommand ReminderManager::parseCommand(const QString &text) const
|
||||
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(
|
||||
const QString &title,
|
||||
const QString &originalText,
|
||||
const QDateTime &remindAt,
|
||||
ReminderItem *createdItem,
|
||||
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())
|
||||
{
|
||||
@@ -97,6 +297,12 @@ bool ReminderManager::createReminder(
|
||||
return false;
|
||||
}
|
||||
|
||||
ReminderRecurrence normalizedRecurrence = recurrence;
|
||||
if (!normalizeRecurrence(&normalizedRecurrence, errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReminderItem item;
|
||||
item.id = nextReminderId();
|
||||
item.title = title.trimmed().isEmpty() ? QStringLiteral("提醒") : title.trimmed();
|
||||
@@ -105,6 +311,11 @@ bool ReminderManager::createReminder(
|
||||
item.status = ReminderStatus::Pending;
|
||||
item.createdAt = QDateTime::currentDateTime();
|
||||
item.soundId.clear();
|
||||
item.recurrence = normalizedRecurrence;
|
||||
if (!reminderIsRecurring(item.recurrence))
|
||||
{
|
||||
item.recurrence = {};
|
||||
}
|
||||
|
||||
m_items.append(item);
|
||||
if (!save(errorMessage))
|
||||
@@ -122,6 +333,33 @@ bool ReminderManager::createReminder(
|
||||
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)
|
||||
{
|
||||
const QString normalizedId = id.trimmed();
|
||||
@@ -129,11 +367,14 @@ bool ReminderManager::cancelReminder(const QString &id, QString *errorMessage)
|
||||
{
|
||||
if (item.id == normalizedId && isPending(item))
|
||||
{
|
||||
const QDateTime previousFinishedAt = item.finishedAt;
|
||||
item.status = ReminderStatus::Canceled;
|
||||
item.finishedAt = QDateTime::currentDateTime();
|
||||
const bool saved = save(errorMessage);
|
||||
if (!saved)
|
||||
{
|
||||
item.status = ReminderStatus::Pending;
|
||||
item.finishedAt = previousFinishedAt;
|
||||
}
|
||||
scheduleNextReminder();
|
||||
return saved;
|
||||
@@ -177,12 +418,15 @@ bool ReminderManager::cancelReminderByQuery(const QString &query, ReminderItem *
|
||||
}
|
||||
|
||||
ReminderItem &item = m_items[matches.first()];
|
||||
const QDateTime previousFinishedAt = item.finishedAt;
|
||||
item.status = ReminderStatus::Canceled;
|
||||
item.finishedAt = QDateTime::currentDateTime();
|
||||
|
||||
const bool saved = save(errorMessage);
|
||||
if (!saved)
|
||||
{
|
||||
item.status = ReminderStatus::Pending;
|
||||
item.finishedAt = previousFinishedAt;
|
||||
}
|
||||
else if (canceledItem != nullptr)
|
||||
{
|
||||
@@ -193,12 +437,19 @@ bool ReminderManager::cancelReminderByQuery(const QString &query, ReminderItem *
|
||||
}
|
||||
|
||||
bool ReminderManager::clearFinishedReminders(QString *errorMessage)
|
||||
{
|
||||
return pruneFinishedReminders(DefaultHistoryRetentionDays, errorMessage);
|
||||
}
|
||||
|
||||
bool ReminderManager::pruneFinishedReminders(int retentionDays, QString *errorMessage)
|
||||
{
|
||||
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)
|
||||
{
|
||||
const ReminderStatus status = m_items.at(index).status;
|
||||
if (status == ReminderStatus::Triggered || status == ReminderStatus::Canceled)
|
||||
const ReminderItem &item = m_items.at(index);
|
||||
if (isFinished(item) && finishedReferenceTime(item) < cutoff)
|
||||
{
|
||||
m_items.removeAt(index);
|
||||
}
|
||||
@@ -219,6 +470,78 @@ bool ReminderManager::clearFinishedReminders(QString *errorMessage)
|
||||
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
|
||||
{
|
||||
const QVector<ReminderItem> reminders = pendingReminders();
|
||||
@@ -231,8 +554,8 @@ QString ReminderManager::pendingReminderSummary() const
|
||||
lines.append(QStringLiteral("当前待提醒:"));
|
||||
for (const ReminderItem &item : reminders)
|
||||
{
|
||||
lines.append(QStringLiteral("%1:%2(%3)")
|
||||
.arg(item.id, item.title, reminderDisplayTime(item.remindAt)));
|
||||
lines.append(QStringLiteral("%1:%2(%3,下一次:%4)")
|
||||
.arg(item.id, item.title, reminderRecurrenceDisplayText(item), reminderDisplayTime(item.remindAt)));
|
||||
}
|
||||
return lines.join(QChar('\n'));
|
||||
}
|
||||
@@ -256,15 +579,39 @@ void ReminderManager::processDueReminders()
|
||||
{
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
QVector<ReminderItem> triggeredItems;
|
||||
QVector<int> triggeredIndexes;
|
||||
const QVector<ReminderItem> previousItems = m_items;
|
||||
for (int index = 0; index < m_items.size(); ++index)
|
||||
{
|
||||
ReminderItem &item = m_items[index];
|
||||
if (isPending(item) && item.remindAt <= now)
|
||||
{
|
||||
ReminderItem triggeredItem = item;
|
||||
triggeredItem.status = ReminderStatus::Triggered;
|
||||
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;
|
||||
triggeredItems.append(item);
|
||||
triggeredIndexes.append(index);
|
||||
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))
|
||||
{
|
||||
Logger::warning(QStringLiteral("Failed to save triggered reminders: ") + errorMessage);
|
||||
for (const int index : triggeredIndexes)
|
||||
{
|
||||
if (index >= 0 && index < m_items.size())
|
||||
{
|
||||
m_items[index].status = ReminderStatus::Pending;
|
||||
}
|
||||
}
|
||||
m_items = previousItems;
|
||||
scheduleNextReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
QVector<ReminderItem> allReminders() const;
|
||||
QVector<ReminderItem> pendingReminders() const;
|
||||
ReminderCommand parseCommand(const QString &text) const;
|
||||
void checkDueRemindersNow();
|
||||
|
||||
bool createReminder(
|
||||
const QString &title,
|
||||
@@ -28,9 +29,28 @@ public:
|
||||
const QDateTime &remindAt,
|
||||
ReminderItem *createdItem = 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 cancelReminderByQuery(const QString &query, ReminderItem *canceledItem = nullptr, 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;
|
||||
|
||||
private:
|
||||
@@ -44,6 +64,7 @@ private:
|
||||
ReminderParser m_parser;
|
||||
QVector<ReminderItem> m_items;
|
||||
QTimer m_timer;
|
||||
QTimer m_guardTimer;
|
||||
TriggeredCallback m_triggeredCallback;
|
||||
bool m_started = false;
|
||||
};
|
||||
|
||||
+247
-15
@@ -182,6 +182,13 @@ struct ReminderDateResolution
|
||||
bool explicitDate = false;
|
||||
};
|
||||
|
||||
struct ParsedReminderTime
|
||||
{
|
||||
int hour = -1;
|
||||
int minute = -1;
|
||||
QString expression;
|
||||
};
|
||||
|
||||
ReminderDateResolution resolveReminderDate(const QString &text, const QDate ¤tDate)
|
||||
{
|
||||
QRegularExpressionMatch match;
|
||||
@@ -254,6 +261,134 @@ QString removeFirst(const QString &text, const QString &part)
|
||||
}
|
||||
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
|
||||
@@ -293,18 +428,15 @@ ReminderCommand ReminderParser::parseCreateCommand(const QString &text, const QD
|
||||
{
|
||||
const QDate currentDate = now.date();
|
||||
|
||||
if (containsAny(text, {
|
||||
QStringLiteral("每天"),
|
||||
QStringLiteral("每日"),
|
||||
QStringLiteral("每周"),
|
||||
QStringLiteral("每星期"),
|
||||
QStringLiteral("每月"),
|
||||
QStringLiteral("每年"),
|
||||
QStringLiteral("工作日"),
|
||||
QStringLiteral("重复"),
|
||||
}))
|
||||
const ReminderCommand recurringCommand = parseRecurringCreateCommand(text, now);
|
||||
if (recurringCommand.type != ReminderCommandType::Invalid || !recurringCommand.errorMessage.isEmpty())
|
||||
{
|
||||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("重复提醒尚未支持,目前只能创建一次性提醒。")};
|
||||
return recurringCommand;
|
||||
}
|
||||
|
||||
if (containsUnsupportedRecurrence(text))
|
||||
{
|
||||
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("暂不支持该重复提醒规则,目前支持每天、每周、每月。")};
|
||||
}
|
||||
|
||||
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点提醒我开会”。")};
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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 = {
|
||||
QStringLiteral("提醒我"),
|
||||
QStringLiteral("提醒"),
|
||||
@@ -462,6 +693,11 @@ QString ReminderParser::extractTitle(QString text, const QString &timeExpression
|
||||
QStringLiteral("今天"),
|
||||
QStringLiteral("明天"),
|
||||
QStringLiteral("后天"),
|
||||
QStringLiteral("每天"),
|
||||
QStringLiteral("每日"),
|
||||
QStringLiteral("每周"),
|
||||
QStringLiteral("每星期"),
|
||||
QStringLiteral("每月"),
|
||||
};
|
||||
|
||||
for (const QString &token : tokensToRemove)
|
||||
@@ -469,10 +705,6 @@ QString ReminderParser::extractTitle(QString text, const QString &timeExpression
|
||||
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();
|
||||
while (text.startsWith(QChar(0x3000)) || text.startsWith(QLatin1Char(' ')) || text.startsWith(QLatin1Char(',')) || text.startsWith(QChar(0xff0c)))
|
||||
{
|
||||
|
||||
@@ -12,5 +12,6 @@ public:
|
||||
|
||||
private:
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -73,6 +73,22 @@ bool readChunkHeader(QDataStream &stream, QByteArray *id, quint32 *size)
|
||||
stream >> *size;
|
||||
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()
|
||||
@@ -227,7 +243,26 @@ bool ReminderSoundRepository::deleteUserSound(const QString &soundId, QString *e
|
||||
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);
|
||||
if (!file.exists())
|
||||
{
|
||||
|
||||
@@ -10,12 +10,80 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace
|
||||
{
|
||||
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 object;
|
||||
@@ -25,7 +93,12 @@ QJsonObject reminderToObject(const ReminderItem &item)
|
||||
object.insert(QStringLiteral("remindAt"), item.remindAt.toString(Qt::ISODate));
|
||||
object.insert(QStringLiteral("status"), reminderStatusToString(item.status));
|
||||
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("recurrence"), recurrenceToObject(item.recurrence));
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -38,7 +111,14 @@ ReminderItem reminderFromObject(const QJsonObject &object)
|
||||
item.remindAt = QDateTime::fromString(object.value(QStringLiteral("remindAt")).toString(), Qt::ISODate);
|
||||
item.status = reminderStatusFromString(object.value(QStringLiteral("status")).toString());
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +178,12 @@ QVector<ReminderItem> ReminderStore::load(QString *errorMessage) const
|
||||
item.createdAt = item.remindAt;
|
||||
}
|
||||
|
||||
if (!item.finishedAt.isValid()
|
||||
&& (item.status == ReminderStatus::Triggered || item.status == ReminderStatus::Canceled))
|
||||
{
|
||||
item.finishedAt = item.remindAt;
|
||||
}
|
||||
|
||||
items.append(item);
|
||||
}
|
||||
|
||||
@@ -125,8 +211,8 @@ bool ReminderStore::save(const QVector<ReminderItem> &items, QString *errorMessa
|
||||
QJsonObject root;
|
||||
root.insert(QStringLiteral("reminders"), reminders);
|
||||
|
||||
QFile file(storePath());
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
QSaveFile file(storePath());
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
if (errorMessage != nullptr)
|
||||
{
|
||||
@@ -136,7 +222,27 @@ bool ReminderStore::save(const QVector<ReminderItem> &items, QString *errorMessa
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -2,6 +2,44 @@
|
||||
|
||||
#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)
|
||||
{
|
||||
switch (status)
|
||||
@@ -33,11 +71,76 @@ ReminderStatus reminderStatusFromString(const QString &status)
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
std::sort(reminders.begin(), reminders.end(), [](const ReminderItem &left, const ReminderItem &right) {
|
||||
|
||||
@@ -19,6 +19,25 @@ enum class ReminderCommandType
|
||||
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
|
||||
{
|
||||
QString id;
|
||||
@@ -27,7 +46,9 @@ struct ReminderItem
|
||||
QDateTime remindAt;
|
||||
ReminderStatus status = ReminderStatus::Pending;
|
||||
QDateTime createdAt;
|
||||
QDateTime finishedAt;
|
||||
QString soundId;
|
||||
ReminderRecurrence recurrence;
|
||||
};
|
||||
|
||||
struct ReminderCommand
|
||||
@@ -38,9 +59,15 @@ struct ReminderCommand
|
||||
QDateTime remindAt;
|
||||
QString cancelQuery;
|
||||
QString errorMessage;
|
||||
ReminderRecurrence recurrence;
|
||||
};
|
||||
|
||||
QString reminderStatusToString(ReminderStatus 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 reminderRecurrenceDisplayText(const ReminderItem &item);
|
||||
QVector<ReminderItem> sortedReminders(QVector<ReminderItem> reminders);
|
||||
|
||||
@@ -62,14 +62,15 @@ void TrayController::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);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TrayController::createMenu()
|
||||
|
||||
@@ -12,7 +12,7 @@ public:
|
||||
|
||||
bool isAvailable() const;
|
||||
void show();
|
||||
void showNotification(const QString &title, const QString &message);
|
||||
bool showNotification(const QString &title, const QString &message);
|
||||
|
||||
private:
|
||||
void createMenu();
|
||||
|
||||
+267
-18
@@ -24,6 +24,7 @@
|
||||
#include <QContextMenuEvent>
|
||||
#include <QCursor>
|
||||
#include <QDialog>
|
||||
#include <QFrame>
|
||||
#include <QGuiApplication>
|
||||
#include <QHideEvent>
|
||||
#include <QList>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <QPixmap>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QRandomGenerator>
|
||||
#include <QScreen>
|
||||
#include <QSet>
|
||||
@@ -333,6 +335,7 @@ void PetWindow::resumeAnimation()
|
||||
|
||||
void PetWindow::showBubbleMessage(const QString &message)
|
||||
{
|
||||
hideReminderActions();
|
||||
m_chatBubble->showMessage(message, bubbleAnchorPosition());
|
||||
}
|
||||
|
||||
@@ -352,8 +355,47 @@ void PetWindow::openSettingsDialog()
|
||||
[this](const QString &reminderId, QString *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) {
|
||||
return m_reminderManager && m_reminderManager->clearFinishedReminders(errorMessage);
|
||||
return m_reminderManager && m_reminderManager->pruneFinishedReminders(20, errorMessage);
|
||||
},
|
||||
[this](const QString &soundId, double volume) {
|
||||
if (m_reminderSoundPlayer)
|
||||
@@ -402,6 +444,10 @@ void PetWindow::activateFromExternalInstance()
|
||||
|
||||
raise();
|
||||
activateWindow();
|
||||
if (m_reminderManager)
|
||||
{
|
||||
m_reminderManager->checkDueRemindersNow();
|
||||
}
|
||||
updateBubblePosition();
|
||||
}
|
||||
|
||||
@@ -410,7 +456,7 @@ void PetWindow::setSettingsFallbackInContextMenuEnabled(bool 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)
|
||||
{
|
||||
@@ -547,37 +593,240 @@ bool PetWindow::handleReminderChatMessage(const QString &message)
|
||||
}
|
||||
|
||||
void PetWindow::handleTriggeredReminder(const ReminderItem &item)
|
||||
{
|
||||
playReminderSound();
|
||||
|
||||
if (shouldNotifyOnlyForReminder())
|
||||
{
|
||||
showReminderNotification(item);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueVisibleTriggeredReminder(item);
|
||||
}
|
||||
|
||||
void PetWindow::playReminderSound()
|
||||
{
|
||||
if (m_appConfig.reminderSoundEnabled && m_reminderSoundPlayer)
|
||||
{
|
||||
m_reminderSoundPlayer->play(m_appConfig.reminderSoundId, m_appConfig.reminderSoundVolume);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isVisible())
|
||||
{
|
||||
void PetWindow::showReminderNotification(const ReminderItem &item)
|
||||
{
|
||||
if (m_notificationDispatcher)
|
||||
{
|
||||
m_notificationDispatcher->showReminder(QStringLiteral("定时提醒"), QStringLiteral("到时间啦:%1").arg(item.title));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_dragging)
|
||||
const bool shown = m_notificationDispatcher->showReminder(QStringLiteral("定时提醒"), QStringLiteral("到时间啦:%1").arg(item.title));
|
||||
if (!shown)
|
||||
{
|
||||
m_deferredTriggeredReminders.append(item);
|
||||
Logger::warning(QStringLiteral("Reminder notification backend unavailable: id=%1").arg(item.id));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ReminderItem item = m_pendingVisibleTriggeredReminders.takeFirst();
|
||||
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)
|
||||
{
|
||||
const QString reminderState = m_clips.contains(QStringLiteral("happy"))
|
||||
? QStringLiteral("happy")
|
||||
: QStringLiteral("talk");
|
||||
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)
|
||||
@@ -620,6 +869,7 @@ bool PetWindow::submitAiChatMessage(const QString &message)
|
||||
|
||||
stopAnimationPrewarm();
|
||||
playState(QStringLiteral("think"), false);
|
||||
hideReminderActions();
|
||||
m_streamingAssistantText.clear();
|
||||
m_streamBubbleUpdateTimer.stop();
|
||||
m_streamingChatActive = true;
|
||||
@@ -844,6 +1094,7 @@ void PetWindow::flushStreamingBubble(bool finalUpdate)
|
||||
return;
|
||||
}
|
||||
|
||||
hideReminderActions();
|
||||
m_chatBubble->showMessage(
|
||||
m_streamingAssistantText,
|
||||
bubbleAnchorPosition(),
|
||||
@@ -888,6 +1139,7 @@ void PetWindow::hideEvent(QHideEvent *event)
|
||||
{
|
||||
m_chatBubble->hideBubble();
|
||||
}
|
||||
hideReminderActions();
|
||||
if (m_chatInputDialog)
|
||||
{
|
||||
m_chatInputDialog->hide();
|
||||
@@ -911,6 +1163,7 @@ void PetWindow::showEvent(QShowEvent *event)
|
||||
if (m_reminderManager)
|
||||
{
|
||||
m_reminderManager->start();
|
||||
m_reminderManager->checkDueRemindersNow();
|
||||
}
|
||||
scheduleAnimationPrewarm();
|
||||
}
|
||||
@@ -976,12 +1229,7 @@ void PetWindow::mouseReleaseEvent(QMouseEvent *event)
|
||||
m_dragging = false;
|
||||
playResolvedState(m_stateMachine.endDrag(), false);
|
||||
scheduleAnimationPrewarm();
|
||||
const QVector<ReminderItem> deferredReminders = m_deferredTriggeredReminders;
|
||||
m_deferredTriggeredReminders.clear();
|
||||
for (const ReminderItem &item : deferredReminders)
|
||||
{
|
||||
showTriggeredReminder(item);
|
||||
}
|
||||
showNextTriggeredReminder();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
@@ -1087,6 +1335,7 @@ void PetWindow::addStateTestActions(QMenu *menu)
|
||||
void PetWindow::updateBubblePosition()
|
||||
{
|
||||
m_chatBubble->updateAnchorPosition(bubbleAnchorPosition());
|
||||
updateReminderActionPosition();
|
||||
}
|
||||
|
||||
QPoint PetWindow::bubbleAnchorPosition() const
|
||||
|
||||
+16
-2
@@ -45,7 +45,7 @@ public:
|
||||
void openSettingsDialog();
|
||||
void activateFromExternalInstance();
|
||||
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 resumeAnimation();
|
||||
void showBubbleMessage(const QString &message);
|
||||
@@ -70,7 +70,18 @@ private:
|
||||
bool submitAiChatMessage(const QString &message);
|
||||
bool handleReminderChatMessage(const QString &message);
|
||||
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 ensureReminderActionPanel();
|
||||
void showReminderActions(const ReminderItem &item);
|
||||
void hideReminderActions();
|
||||
void updateReminderActionPosition();
|
||||
void snoozeTriggeredReminder(const ReminderItem &item);
|
||||
void clearConversation();
|
||||
void cancelActiveAIRequest();
|
||||
void showConversationHistory();
|
||||
@@ -121,6 +132,7 @@ private:
|
||||
std::unique_ptr<NotificationDispatcher> m_notificationDispatcher;
|
||||
std::unique_ptr<ReminderManager> m_reminderManager;
|
||||
std::unique_ptr<ReminderSoundPlayer> m_reminderSoundPlayer;
|
||||
std::unique_ptr<QWidget> m_reminderActionPanel;
|
||||
PetView *m_petView;
|
||||
QTimer m_idleBehaviorTimer;
|
||||
QTimer m_behaviorReturnTimer;
|
||||
@@ -136,7 +148,8 @@ private:
|
||||
QPoint m_dragOffset;
|
||||
QString m_streamingAssistantText;
|
||||
QStringList m_animationPrewarmQueue;
|
||||
QVector<ReminderItem> m_deferredTriggeredReminders;
|
||||
QVector<ReminderItem> m_pendingVisibleTriggeredReminders;
|
||||
ReminderItem m_activeTriggeredReminder;
|
||||
qint64 m_clipAccessSerial = 0;
|
||||
bool m_dragging;
|
||||
bool m_alwaysOnTop;
|
||||
@@ -145,4 +158,5 @@ private:
|
||||
bool m_streamingChatActive = false;
|
||||
bool m_streamingTalkStarted = false;
|
||||
bool m_settingsFallbackInContextMenuEnabled = true;
|
||||
bool m_hasActiveTriggeredReminder = false;
|
||||
};
|
||||
|
||||
+239
-13
@@ -9,6 +9,7 @@
|
||||
#include <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDateTimeEdit>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFileDialog>
|
||||
@@ -30,6 +31,7 @@
|
||||
#include <QStackedWidget>
|
||||
#include <QStyle>
|
||||
#include <QStringList>
|
||||
#include <QTime>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
@@ -38,6 +40,8 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int ReminderHistoryRetentionDays = 20;
|
||||
|
||||
QString normalizedProviderName(const QString &provider)
|
||||
{
|
||||
const QString normalized = provider.trimmed().toLower();
|
||||
@@ -105,11 +109,22 @@ bool reminderMatchesStatusFilter(const ReminderItem &item, const QString &filter
|
||||
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)
|
||||
{
|
||||
if (item.status == ReminderStatus::Triggered || item.status == ReminderStatus::Canceled)
|
||||
if (isFinishedReminder(item) && reminderFinishedReferenceTime(item) < cutoff)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -117,6 +132,158 @@ bool hasFinishedReminders(const QVector<ReminderItem> &reminders)
|
||||
|
||||
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(
|
||||
@@ -126,6 +293,7 @@ SettingsDialog::SettingsDialog(
|
||||
std::function<bool()> aiTestBlocked,
|
||||
std::function<void()> clearConversationHistoryCallback,
|
||||
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<void(const QString &, double)> playReminderSoundCallback,
|
||||
QWidget *parent)
|
||||
@@ -159,7 +327,8 @@ SettingsDialog::SettingsDialog(
|
||||
, m_reminderStatusFilterComboBox(new QComboBox(this))
|
||||
, m_reminderListWidget(new QListWidget(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_reminderSoundEnabledCheckBox(new QCheckBox(QStringLiteral("启用提醒音效"), this))
|
||||
, m_reminderSoundVolumeSpinBox(new QSpinBox(this))
|
||||
@@ -174,6 +343,7 @@ SettingsDialog::SettingsDialog(
|
||||
, m_aiTestBlocked(std::move(aiTestBlocked))
|
||||
, m_clearConversationHistory(std::move(clearConversationHistoryCallback))
|
||||
, m_cancelReminder(std::move(cancelReminderCallback))
|
||||
, m_updateReminder(std::move(updateReminderCallback))
|
||||
, m_clearFinishedReminders(std::move(clearFinishedRemindersCallback))
|
||||
, m_playReminderSound(std::move(playReminderSoundCallback))
|
||||
{
|
||||
@@ -378,7 +548,7 @@ SettingsDialog::SettingsDialog(
|
||||
|
||||
auto *reminderTitleLabel = new QLabel(QStringLiteral("提醒"), this);
|
||||
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->setWordWrap(true);
|
||||
|
||||
@@ -392,6 +562,7 @@ SettingsDialog::SettingsDialog(
|
||||
reminderListLayout->addWidget(m_reminderListWidget);
|
||||
auto *reminderActionLayout = new QHBoxLayout();
|
||||
reminderActionLayout->addWidget(m_cancelReminderButton);
|
||||
reminderActionLayout->addWidget(m_editReminderButton);
|
||||
reminderActionLayout->addWidget(m_clearFinishedRemindersButton);
|
||||
reminderActionLayout->addWidget(m_reminderStatusLabel, 1);
|
||||
reminderListLayout->addLayout(reminderActionLayout);
|
||||
@@ -572,6 +743,9 @@ SettingsDialog::SettingsDialog(
|
||||
connect(m_cancelReminderButton, &QPushButton::clicked, this, [this]() {
|
||||
cancelSelectedReminder();
|
||||
});
|
||||
connect(m_editReminderButton, &QPushButton::clicked, this, [this]() {
|
||||
editSelectedReminder();
|
||||
});
|
||||
connect(m_clearFinishedRemindersButton, &QPushButton::clicked, this, [this]() {
|
||||
clearFinishedReminders();
|
||||
});
|
||||
@@ -967,8 +1141,8 @@ void SettingsDialog::reloadReminderList()
|
||||
}
|
||||
|
||||
auto *listItem = new QListWidgetItem(
|
||||
QStringLiteral("%1 %2 %3 %4")
|
||||
.arg(reminderDisplayTime(item.remindAt), reminderStatusDisplayText(item.status), item.title, item.originalText),
|
||||
QStringLiteral("%1 %2 %3 %4 %5")
|
||||
.arg(reminderDisplayTime(item.remindAt), reminderStatusDisplayText(item.status), reminderRecurrenceDisplayText(item), item.title, item.originalText),
|
||||
m_reminderListWidget);
|
||||
listItem->setData(Qt::UserRole, item.id);
|
||||
listItem->setData(Qt::UserRole + 1, reminderStatusToString(item.status));
|
||||
@@ -1042,7 +1216,8 @@ void SettingsDialog::updateReminderActionButtons()
|
||||
const bool selectedPending = item != nullptr
|
||||
&& item->data(Qt::UserRole + 1).toString() == QStringLiteral("pending");
|
||||
m_cancelReminderButton->setEnabled(selectedPending);
|
||||
m_clearFinishedRemindersButton->setEnabled(hasFinishedReminders(m_reminders));
|
||||
m_editReminderButton->setEnabled(selectedPending);
|
||||
m_clearFinishedRemindersButton->setEnabled(hasPrunableFinishedReminders(m_reminders, ReminderHistoryRetentionDays));
|
||||
}
|
||||
|
||||
void SettingsDialog::cancelSelectedReminder()
|
||||
@@ -1078,6 +1253,7 @@ void SettingsDialog::cancelSelectedReminder()
|
||||
if (reminder.id == reminderId)
|
||||
{
|
||||
reminder.status = ReminderStatus::Canceled;
|
||||
reminder.finishedAt = QDateTime::currentDateTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1085,18 +1261,68 @@ void SettingsDialog::cancelSelectedReminder()
|
||||
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()
|
||||
{
|
||||
if (!hasFinishedReminders(m_reminders))
|
||||
if (!hasPrunableFinishedReminders(m_reminders, ReminderHistoryRetentionDays))
|
||||
{
|
||||
m_reminderStatusLabel->setText(QStringLiteral("没有可清理的历史提醒。"));
|
||||
m_reminderStatusLabel->setText(QStringLiteral("没有可清理的20天前历史提醒。"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QMessageBox::StandardButton result = QMessageBox::warning(
|
||||
this,
|
||||
QStringLiteral("清理提醒历史"),
|
||||
QStringLiteral("确定要清理所有已触发和已取消的提醒记录吗?\n\n待提醒事项不会被删除。"),
|
||||
QStringLiteral("确定要清理20天前的已触发和已取消提醒记录吗?\n\n最近20天历史会保留,待提醒事项不会被删除。"),
|
||||
QMessageBox::Yes | QMessageBox::Cancel,
|
||||
QMessageBox::Cancel);
|
||||
if (result != QMessageBox::Yes)
|
||||
@@ -1116,15 +1342,15 @@ void SettingsDialog::clearFinishedReminders()
|
||||
|
||||
for (int index = m_reminders.size() - 1; index >= 0; --index)
|
||||
{
|
||||
const ReminderStatus status = m_reminders.at(index).status;
|
||||
if (status == ReminderStatus::Triggered || status == ReminderStatus::Canceled)
|
||||
if (isFinishedReminder(m_reminders.at(index))
|
||||
&& reminderFinishedReferenceTime(m_reminders.at(index)) < QDateTime::currentDateTime().addDays(-ReminderHistoryRetentionDays))
|
||||
{
|
||||
m_reminders.removeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
reloadReminderList();
|
||||
m_reminderStatusLabel->setText(QStringLiteral("已清理提醒历史。"));
|
||||
m_reminderStatusLabel->setText(QStringLiteral("已清理20天前提醒历史。"));
|
||||
}
|
||||
|
||||
void SettingsDialog::importReminderSound()
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
std::function<bool()> aiTestBlocked,
|
||||
std::function<void()> clearConversationHistoryCallback,
|
||||
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<void(const QString &, double)> playReminderSoundCallback,
|
||||
QWidget *parent = nullptr);
|
||||
@@ -61,6 +62,7 @@ private:
|
||||
void updateReminderSoundButtons();
|
||||
void updateReminderActionButtons();
|
||||
void cancelSelectedReminder();
|
||||
void editSelectedReminder();
|
||||
void clearFinishedReminders();
|
||||
void importReminderSound();
|
||||
void deleteSelectedReminderSound();
|
||||
@@ -97,6 +99,7 @@ private:
|
||||
QComboBox *m_reminderStatusFilterComboBox = nullptr;
|
||||
QListWidget *m_reminderListWidget = nullptr;
|
||||
QPushButton *m_cancelReminderButton = nullptr;
|
||||
QPushButton *m_editReminderButton = nullptr;
|
||||
QPushButton *m_clearFinishedRemindersButton = nullptr;
|
||||
QLabel *m_reminderStatusLabel = nullptr;
|
||||
QCheckBox *m_reminderSoundEnabledCheckBox = nullptr;
|
||||
@@ -114,6 +117,7 @@ private:
|
||||
std::function<bool()> m_aiTestBlocked;
|
||||
std::function<void()> m_clearConversationHistory;
|
||||
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<void(const QString &, double)> m_playReminderSound;
|
||||
std::unique_ptr<LLMProvider> m_testProvider;
|
||||
|
||||
Reference in New Issue
Block a user