refactor: 重构提醒通知系统

This commit is contained in:
zl-q
2026-04-01 00:42:34 +08:00
parent 9a231dae9e
commit 6722f3d74b
21 changed files with 375 additions and 171 deletions
@@ -14,9 +14,15 @@ class ReminderNotificationRouter {
Stream<ReminderNotificationTap> get taps => _controller.stream;
Future<void> start() async {
await _scheduler.initialize(onTap: _controller.add);
await _scheduler.initialize(
onTap: (tap) {
_scheduler.cancelReminder(tap);
_controller.add(tap);
},
);
final launchTap = await _scheduler.consumeLaunchTap();
if (launchTap != null) {
await _scheduler.cancelReminder(launchTap);
_controller.add(launchTap);
}
}
@@ -2,20 +2,37 @@ import '../models/reminder_alarm.dart';
import 'reminder_scheduler_service.dart';
class ReminderReconcileService {
const ReminderReconcileService({required ReminderSchedulerService scheduler})
: _scheduler = scheduler;
ReminderReconcileService({
required ReminderSchedulerService scheduler,
DateTime Function()? nowProvider,
}) : _scheduler = scheduler,
_now = nowProvider ?? DateTime.now;
final ReminderSchedulerService _scheduler;
final DateTime Function() _now;
final Map<String, DateTime> _snoozeSuppressUntilByEventId =
<String, DateTime>{};
Future<void> reconcileEvent(
ReminderEventSnapshot event, {
DateTime? now,
}) async {
final current = now ?? _now();
if (event.isArchived || event.reminderMinutes == null) {
_snoozeSuppressUntilByEventId.remove(event.eventId);
await _scheduler.cancelEventReminders(event.eventId);
return;
}
await _scheduler.upsertEventReminders(event, now: now);
final suppressUntil = _snoozeSuppressUntilByEventId[event.eventId];
if (suppressUntil != null) {
if (current.isBefore(suppressUntil)) {
return;
}
_snoozeSuppressUntilByEventId.remove(event.eventId);
}
await _scheduler.upsertEventReminders(event, now: current);
}
Future<void> reconcileEvents(
@@ -28,11 +45,19 @@ class ReminderReconcileService {
}
Future<void> archiveAndCancel(String eventId) {
_snoozeSuppressUntilByEventId.remove(eventId);
return _scheduler.cancelEventReminders(eventId);
}
Future<void> snooze10m(ReminderEventSnapshot event) async {
await _scheduler.cancelEventReminders(event.eventId);
await _scheduler.scheduleSingleSnooze(event);
final firstFireAt = _now().add(const Duration(minutes: 10));
final alarms = ReminderSchedulerService.buildAlarmSeries(
event: event,
firstFireAt: firstFireAt,
interval: const Duration(minutes: 10),
);
await _scheduler.scheduleAlarms(alarms);
_snoozeSuppressUntilByEventId[event.eventId] = firstFireAt;
}
}
@@ -18,6 +18,8 @@ class ReminderSchedulerService {
'Alarm-style notifications for scheduled events';
static const String _androidSoundResource = 'reminder_1';
static const String _iosSoundFile = 'reminder_1.wav';
static const int _maxAlarmIntervals = 144; // 24 hours at 10-minute intervals
static const Duration _scheduleLeadTime = Duration(seconds: 2);
final FlutterLocalNotificationsPlugin _plugin;
final List<void Function(ReminderNotificationTap tap)> _tapCallbacks = [];
@@ -140,32 +142,6 @@ class ReminderSchedulerService {
}
}
Future<void> scheduleSingleSnooze(
ReminderEventSnapshot event, {
Duration delay = const Duration(minutes: 10),
DateTime? now,
}) async {
await _ensureInitialized();
final current = now ?? DateTime.now();
final fireAt = current.add(delay);
if (event.endAt != null && fireAt.isAfter(event.endAt!)) {
return;
}
final alarm = ReminderAlarm(
eventId: event.eventId,
title: event.title,
startAt: event.startAt,
endAt: event.endAt,
timezone: event.timezone,
reminderMinutes: event.reminderMinutes ?? 0,
fireAt: fireAt,
fireTimeBucket: _toBucket(fireAt),
location: event.location,
notes: event.notes,
);
await _scheduleAlarm(alarm);
}
Future<void> cancelEventReminders(String eventId) async {
await _ensureInitialized();
final pending = await _plugin.pendingNotificationRequests();
@@ -185,6 +161,12 @@ class ReminderSchedulerService {
return _ensureInitialized().then((_) => _plugin.cancelAll());
}
Future<void> cancelReminder(ReminderNotificationTap tap) async {
await _ensureInitialized();
final id = _notificationId(tap.eventId, tap.fireTimeBucket);
await _plugin.cancel(id);
}
Future<void> _ensureInitialized() {
if (_initialized) {
return Future<void>.value();
@@ -212,17 +194,53 @@ class ReminderSchedulerService {
return const [];
}
final List<ReminderAlarm> alarms = [];
DateTime fireAt;
final firstFireAt = _nextFireTimeAfter(
current: current,
remindAt: remindAt,
endAt: endAt,
);
if (current.isBefore(remindAt)) {
fireAt = remindAt;
} else {
fireAt = current.add(const Duration(seconds: 5));
if (firstFireAt == null) {
return const [];
}
return buildAlarmSeries(
event: event,
firstFireAt: firstFireAt,
interval: const Duration(minutes: 10),
);
}
static DateTime? _nextFireTimeAfter({
required DateTime current,
required DateTime remindAt,
required DateTime? endAt,
}) {
final earliest = current.add(_scheduleLeadTime);
final next = remindAt.isAfter(earliest) ? remindAt : earliest;
if (endAt != null && next.isAfter(endAt)) {
return null;
}
return next;
}
static List<ReminderAlarm> buildAlarmSeries({
required ReminderEventSnapshot event,
required DateTime firstFireAt,
Duration interval = const Duration(minutes: 10),
}) {
final endAt = event.endAt;
if (endAt != null && firstFireAt.isAfter(endAt)) {
return const [];
}
final alarms = <ReminderAlarm>[];
var fireAt = firstFireAt;
var iterations = 0;
while (iterations < 144) {
while (iterations < _maxAlarmIntervals) {
if (endAt != null && fireAt.isAfter(endAt)) {
break;
}
@@ -233,7 +251,7 @@ class ReminderSchedulerService {
startAt: event.startAt,
endAt: endAt,
timezone: event.timezone,
reminderMinutes: reminderMinutes,
reminderMinutes: event.reminderMinutes ?? 0,
fireAt: fireAt,
fireTimeBucket: _toBucket(fireAt),
location: event.location,
@@ -244,15 +262,30 @@ class ReminderSchedulerService {
if (endAt == null) {
break;
}
fireAt = fireAt.add(const Duration(minutes: 10));
fireAt = fireAt.add(interval);
iterations += 1;
}
return alarms;
}
Future<void> scheduleAlarms(List<ReminderAlarm> alarms) async {
for (final alarm in alarms) {
await _scheduleAlarm(alarm);
}
}
Future<void> _scheduleAlarm(ReminderAlarm alarm) async {
final location = _safeLocation(alarm.timezone);
final fireAt = tz.TZDateTime.from(alarm.fireAt, location);
final nowInTz = tz.TZDateTime.now(location);
var fireAt = tz.TZDateTime.from(alarm.fireAt, location);
final minAllowed = nowInTz.add(_scheduleLeadTime);
if (!fireAt.isAfter(minAllowed)) {
fireAt = minAllowed;
}
if (alarm.endAt != null &&
fireAt.isAfter(tz.TZDateTime.from(alarm.endAt!, location))) {
return;
}
final payload = jsonEncode(alarm.toJson());
final id = _notificationId(alarm.eventId, alarm.fireTimeBucket);
@@ -340,7 +373,7 @@ class ReminderSchedulerService {
return Map<String, dynamic>.from(decoded);
}
} catch (_) {
return const {};
// ignore malformed payload
}
return const {};
}