feat: add reminder scheduling and sound controls

This commit is contained in:
2026-06-01 21:01:11 +08:00
parent 4a7b739eea
commit c794e32023
36 changed files with 2494 additions and 35 deletions
+484
View File
@@ -0,0 +1,484 @@
#include "ReminderParser.h"
#include <QDate>
#include <QRegularExpression>
#include <QStringList>
#include <QTime>
namespace
{
bool containsAny(const QString &text, const QStringList &keywords)
{
for (const QString &keyword : keywords)
{
if (text.contains(keyword))
{
return true;
}
}
return false;
}
int chineseDigitValue(QChar value)
{
if (value == QLatin1Char('0') || value == QChar(0x3007) || value == QStringLiteral("").at(0))
{
return 0;
}
if (value == QStringLiteral("").at(0))
{
return 1;
}
if (value == QStringLiteral("").at(0) || value == QStringLiteral("").at(0))
{
return 2;
}
if (value == QStringLiteral("").at(0))
{
return 3;
}
if (value == QStringLiteral("").at(0))
{
return 4;
}
if (value == QStringLiteral("").at(0))
{
return 5;
}
if (value == QStringLiteral("").at(0))
{
return 6;
}
if (value == QStringLiteral("").at(0))
{
return 7;
}
if (value == QStringLiteral("").at(0))
{
return 8;
}
if (value == QStringLiteral("").at(0))
{
return 9;
}
return -1;
}
int parseSmallInteger(QString value)
{
value = value.trimmed();
value.remove(QStringLiteral(""));
if (value.isEmpty())
{
return -1;
}
bool ok = false;
const int numericValue = value.toInt(&ok);
if (ok)
{
return numericValue;
}
const int tenIndex = value.indexOf(QStringLiteral(""));
if (tenIndex >= 0)
{
const QString left = value.left(tenIndex);
const QString right = value.mid(tenIndex + 1);
const int tens = left.isEmpty() ? 1 : parseSmallInteger(left);
const int ones = right.isEmpty() ? 0 : parseSmallInteger(right);
if (tens < 0 || ones < 0)
{
return -1;
}
return tens * 10 + ones;
}
if (value.size() == 1)
{
return chineseDigitValue(value.at(0));
}
return -1;
}
QString cleanedText(QString text)
{
return text
.replace(QChar(0xff0c), QStringLiteral(" "))
.replace(QChar(0x3002), QStringLiteral(" "))
.replace(QChar(0xff1a), QStringLiteral(":"))
.replace(QChar(0xff1b), QStringLiteral(" "))
.trimmed();
}
int adjustedHour(const QString &period, int hour)
{
if (hour < 0)
{
return -1;
}
if (period == QStringLiteral("下午") || period == QStringLiteral("晚上"))
{
return hour < 12 ? hour + 12 : hour;
}
if (period == QStringLiteral("中午"))
{
return hour < 11 ? hour + 12 : hour;
}
if (period == QStringLiteral("凌晨") && hour == 12)
{
return 0;
}
return hour;
}
int weekdayFromText(const QString &text)
{
const QString normalized = text.trimmed();
if (normalized == QStringLiteral("") || normalized == QStringLiteral("1"))
{
return 1;
}
if (normalized == QStringLiteral("") || normalized == QStringLiteral("2"))
{
return 2;
}
if (normalized == QStringLiteral("") || normalized == QStringLiteral("3"))
{
return 3;
}
if (normalized == QStringLiteral("") || normalized == QStringLiteral("4"))
{
return 4;
}
if (normalized == QStringLiteral("") || normalized == QStringLiteral("5"))
{
return 5;
}
if (normalized == QStringLiteral("") || normalized == QStringLiteral("6"))
{
return 6;
}
if (normalized == QStringLiteral("")
|| normalized == QStringLiteral("")
|| normalized == QStringLiteral("7"))
{
return 7;
}
return -1;
}
struct ReminderDateResolution
{
QDate date;
bool explicitDate = false;
};
ReminderDateResolution resolveReminderDate(const QString &text, const QDate &currentDate)
{
QRegularExpressionMatch match;
const QRegularExpression nextWeekExpression(QStringLiteral("下周\\s*([一二三四五六日天1-7])"));
match = nextWeekExpression.match(text);
if (match.hasMatch())
{
const int targetWeekday = weekdayFromText(match.captured(1));
if (targetWeekday > 0)
{
const int daysToNextMonday = 8 - currentDate.dayOfWeek();
return {currentDate.addDays(daysToNextMonday + targetWeekday - 1), true};
}
}
const QRegularExpression monthDayExpression(QStringLiteral("(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*(?:日|号)?"));
match = monthDayExpression.match(text);
if (match.hasMatch())
{
const int month = match.captured(1).toInt();
const int day = match.captured(2).toInt();
QDate date(currentDate.year(), month, day);
if (date.isValid() && date < currentDate)
{
date = date.addYears(1);
}
return {date, true};
}
const QRegularExpression slashDateExpression(QStringLiteral("(\\d{1,2})\\s*/\\s*(\\d{1,2})"));
match = slashDateExpression.match(text);
if (match.hasMatch())
{
const int month = match.captured(1).toInt();
const int day = match.captured(2).toInt();
QDate date(currentDate.year(), month, day);
if (date.isValid() && date < currentDate)
{
date = date.addYears(1);
}
return {date, true};
}
if (text.contains(QStringLiteral("后天")))
{
return {currentDate.addDays(2), true};
}
if (text.contains(QStringLiteral("明天")))
{
return {currentDate.addDays(1), true};
}
if (text.contains(QStringLiteral("今天")))
{
return {currentDate, true};
}
return {currentDate, false};
}
QString removeFirst(const QString &text, const QString &part)
{
QString result = text;
const int index = result.indexOf(part);
if (index >= 0)
{
result.remove(index, part.size());
}
return result;
}
}
ReminderCommand ReminderParser::parse(const QString &text, const QDateTime &now) const
{
const QString normalized = cleanedText(text);
if (normalized.isEmpty())
{
return {ReminderCommandType::Invalid, {}, {}, {}, {}, QStringLiteral("提醒内容为空。")};
}
if (containsAny(normalized, {QStringLiteral("提醒列表"), QStringLiteral("查看提醒"), QStringLiteral("我的提醒"), QStringLiteral("有哪些提醒")}))
{
return {ReminderCommandType::List, {}, normalized};
}
if (normalized.contains(QStringLiteral("取消提醒")) || normalized.startsWith(QStringLiteral("取消")))
{
QString query = normalized;
query.remove(QStringLiteral("取消提醒"));
if (query == normalized)
{
query.remove(QStringLiteral("取消"));
query.remove(QStringLiteral("提醒"));
}
query = query.trimmed();
if (query.isEmpty())
{
return {ReminderCommandType::Invalid, {}, normalized, {}, {}, QStringLiteral("请说明要取消哪条提醒。")};
}
return {ReminderCommandType::Cancel, {}, normalized, {}, query};
}
return parseCreateCommand(normalized, now);
}
ReminderCommand ReminderParser::parseCreateCommand(const QString &text, const QDateTime &now) const
{
const QDate currentDate = now.date();
if (containsAny(text, {
QStringLiteral("每天"),
QStringLiteral("每日"),
QStringLiteral("每周"),
QStringLiteral("每星期"),
QStringLiteral("每月"),
QStringLiteral("每年"),
QStringLiteral("工作日"),
QStringLiteral("重复"),
}))
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("重复提醒尚未支持,目前只能创建一次性提醒。")};
}
const QRegularExpression relativeMinutesExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*"));
QRegularExpressionMatch match = relativeMinutesExpression.match(text);
if (match.hasMatch())
{
const int minutes = parseSmallInteger(match.captured(1));
if (minutes <= 0)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
}
const QString timeExpression = match.captured(0);
return {
ReminderCommandType::Create,
extractTitle(text, timeExpression),
text,
now.addSecs(minutes * 60),
{},
{},
};
}
const QRegularExpression relativeOneAndHalfHourExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*(?:)?"));
match = relativeOneAndHalfHourExpression.match(text);
if (match.hasMatch())
{
const int hours = parseSmallInteger(match.captured(1));
if (hours < 0)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到有效提醒时间。")};
}
const QString timeExpression = match.captured(0);
return {
ReminderCommandType::Create,
extractTitle(text, timeExpression),
text,
now.addSecs(hours * 60 * 60 + 30 * 60),
{},
{},
};
}
if (text.contains(QStringLiteral("半小时后")))
{
const QString timeExpression = QStringLiteral("半小时后");
return {
ReminderCommandType::Create,
extractTitle(text, timeExpression),
text,
now.addSecs(30 * 60),
{},
{},
};
}
const QRegularExpression relativeHoursExpression(QStringLiteral("([0-9]+|[一二两三四五六七八九十]+)\\s*(?:)?"));
match = relativeHoursExpression.match(text);
if (match.hasMatch())
{
const int hours = parseSmallInteger(match.captured(1));
if (hours <= 0)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
}
const QString timeExpression = match.captured(0);
return {
ReminderCommandType::Create,
extractTitle(text, timeExpression),
text,
now.addSecs(hours * 60 * 60),
{},
{},
};
}
const ReminderDateResolution dateResolution = resolveReminderDate(text, currentDate);
if (!dateResolution.date.isValid())
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到有效提醒日期。")};
}
const QRegularExpression clockExpression(QStringLiteral("(上午|早上|下午|晚上|中午|凌晨)?\\s*([0-9]{1,2}|[]+)\\s*\\s*(?:(\\d{1,2})\\s*?)?"));
match = clockExpression.match(text);
if (match.hasMatch())
{
const QString period = match.captured(1);
int hour = adjustedHour(period, parseSmallInteger(match.captured(2)));
const int minute = match.captured(3).isEmpty() ? 0 : match.captured(3).toInt();
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到有效提醒时间。")};
}
QDateTime remindAt(dateResolution.date, QTime(hour, minute));
if (remindAt <= now)
{
if (dateResolution.explicitDate)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
}
remindAt = remindAt.addDays(1);
}
return {
ReminderCommandType::Create,
extractTitle(text, match.captured(0)),
text,
remindAt,
{},
{},
};
}
const QRegularExpression colonClockExpression(QStringLiteral("(明天)?\\s*(?:)?\\s*([01]?\\d|2[0-3])\\s*[:]\\s*([0-5]\\d)"));
match = colonClockExpression.match(text);
if (match.hasMatch())
{
QDateTime remindAt(dateResolution.date, QTime(match.captured(2).toInt(), match.captured(3).toInt()));
if (remindAt <= now)
{
if (dateResolution.explicitDate)
{
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("提醒时间必须晚于当前时间。")};
}
remindAt = remindAt.addDays(1);
}
return {
ReminderCommandType::Create,
extractTitle(text, match.captured(0)),
text,
remindAt,
{},
{},
};
}
return {ReminderCommandType::Invalid, {}, text, {}, {}, QStringLiteral("没有识别到提醒时间。支持如“10分钟后提醒我喝水”“明天9点提醒我开会”。")};
}
QString ReminderParser::extractTitle(QString text, const QString &timeExpression) const
{
text = removeFirst(text, timeExpression);
const QStringList tokensToRemove = {
QStringLiteral("提醒我"),
QStringLiteral("提醒"),
QStringLiteral("叫我"),
QStringLiteral("到点"),
QStringLiteral("的时候"),
QStringLiteral(""),
QStringLiteral("帮我"),
QStringLiteral("今天"),
QStringLiteral("明天"),
QStringLiteral("后天"),
};
for (const QString &token : tokensToRemove)
{
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)))
{
text.remove(0, 1);
text = text.trimmed();
}
return text.isEmpty() ? QStringLiteral("提醒") : text;
}