feat: 重构会话管理与提醒通知系统

This commit is contained in:
qzl
2026-03-31 18:26:36 +08:00
parent a8c262e9c7
commit 9a231dae9e
31 changed files with 650 additions and 223 deletions
@@ -12,10 +12,12 @@ class ReminderSchedulerService {
ReminderSchedulerService({FlutterLocalNotificationsPlugin? plugin})
: _plugin = plugin ?? FlutterLocalNotificationsPlugin();
static const String _channelId = 'calendar_reminder_alarm_v2';
static const String _channelId = 'calendar_reminder_alarm_v3';
static const String _channelName = 'Schedule alarm';
static const String _channelDescription =
'Alarm-style notifications for scheduled events';
static const String _androidSoundResource = 'reminder_1';
static const String _iosSoundFile = 'reminder_1.wav';
final FlutterLocalNotificationsPlugin _plugin;
final List<void Function(ReminderNotificationTap tap)> _tapCallbacks = [];
@@ -45,6 +47,8 @@ class ReminderSchedulerService {
_channelName,
description: _channelDescription,
importance: Importance.max,
playSound: true,
sound: RawResourceAndroidNotificationSound(_androidSoundResource),
),
);
@@ -120,9 +124,18 @@ class ReminderSchedulerService {
DateTime? now,
}) async {
await _ensureInitialized();
await cancelEventReminders(event.eventId);
final alarms = buildAlarmsForEvent(event, now: now);
for (final alarm in alarms) {
final pending = await _plugin.pendingNotificationRequests();
final plan = buildUpsertPlan(
eventId: event.eventId,
pending: pending,
desired: alarms,
);
for (final id in plan.idsToCancel) {
await _plugin.cancel(id);
}
for (final alarm in plan.alarmsToSchedule) {
await _scheduleAlarm(alarm);
}
}
@@ -252,6 +265,7 @@ class ReminderSchedulerService {
category: AndroidNotificationCategory.alarm,
timeoutAfter: 15000,
playSound: true,
sound: RawResourceAndroidNotificationSound(_androidSoundResource),
enableVibration: true,
fullScreenIntent: false,
ticker: 'calendar-reminder',
@@ -259,6 +273,7 @@ class ReminderSchedulerService {
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentSound: true,
sound: _iosSoundFile,
interruptionLevel: InterruptionLevel.timeSensitive,
);
@@ -344,6 +359,77 @@ class ReminderSchedulerService {
return hash;
}
static int notificationIdFor(String eventId, int fireTimeBucket) {
return _notificationId(eventId, fireTimeBucket);
}
static ReminderUpsertPlan buildUpsertPlan({
required String eventId,
required List<PendingNotificationRequest> pending,
required List<ReminderAlarm> desired,
}) {
final desiredById = <int, ReminderAlarm>{
for (final alarm in desired)
_notificationId(alarm.eventId, alarm.fireTimeBucket): alarm,
};
final existingPayloadById = <int, Map<String, dynamic>>{};
for (final request in pending) {
final raw = request.payload;
if (raw == null || raw.isEmpty) {
continue;
}
final payload = _parsePayload(raw);
if (payload['eventId'] != eventId) {
continue;
}
existingPayloadById[request.id] = payload;
}
final idsToCancel = <int>[];
for (final entry in existingPayloadById.entries) {
final desiredAlarm = desiredById[entry.key];
if (desiredAlarm == null) {
idsToCancel.add(entry.key);
continue;
}
if (!_matchesAlarmPayload(entry.value, desiredAlarm)) {
idsToCancel.add(entry.key);
}
}
final alarmsToSchedule = <ReminderAlarm>[];
for (final entry in desiredById.entries) {
final existingPayload = existingPayloadById[entry.key];
if (existingPayload == null ||
!_matchesAlarmPayload(existingPayload, entry.value)) {
alarmsToSchedule.add(entry.value);
}
}
return ReminderUpsertPlan(
idsToCancel: idsToCancel,
alarmsToSchedule: alarmsToSchedule,
);
}
static bool _matchesAlarmPayload(
Map<String, dynamic> payload,
ReminderAlarm alarm,
) {
return payload['eventId'] == alarm.eventId &&
payload['title'] == alarm.title &&
payload['startAt'] == alarm.startAt.toIso8601String() &&
payload['endAt'] == alarm.endAt?.toIso8601String() &&
payload['timezone'] == alarm.timezone &&
payload['reminderMinutes'] == alarm.reminderMinutes &&
payload['fireAt'] == alarm.fireAt.toIso8601String() &&
payload['fireTimeBucket'] == alarm.fireTimeBucket &&
payload['location'] == alarm.location &&
payload['notes'] == alarm.notes &&
payload['version'] == alarm.version;
}
static tz.Location _safeLocation(String timezone) {
try {
return tz.getLocation(timezone);
@@ -353,6 +439,16 @@ class ReminderSchedulerService {
}
}
class ReminderUpsertPlan {
const ReminderUpsertPlan({
required this.idsToCancel,
required this.alarmsToSchedule,
});
final List<int> idsToCancel;
final List<ReminderAlarm> alarmsToSchedule;
}
@pragma('vm:entry-point')
void _onBackgroundTap(NotificationResponse response) {
debugPrint('Background reminder tap received: ${response.payload}');