import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../domain/app_snapshot.dart'; import '../domain/app_settings.dart'; import '../data/snapshot_store.dart'; import '../data/json_schedule_import.dart'; import '../services/notification_service.dart'; import '../services/diagnostic_log.dart'; import '../services/calendar_service.dart'; import '../domain/schedule_engine.dart'; import '../data/demo_schedule.dart'; import '../domain/exam_schedule.dart'; import '../domain/school_term.dart'; import '../domain/school_identity.dart'; class AppController extends ChangeNotifier { AppController(this.store, this.notifications, this.calendar, this.clock); final SnapshotStore store; final NotificationService notifications; final CalendarService calendar; final DateTime Function() clock; AppSnapshot snapshot = AppSnapshot(); bool isDemo = false; bool storageReady = true; List _demo = []; AppSettings? _demoSettings; Future _queue = Future.value(); String? lastError, calendarSyncError; String? reminderErrorCode; ReminderResult reminderResult = const ReminderResult(ReminderStatus.off); List get visibleCourses => isDemo ? _demo : snapshot.courses; AppSettings get visibleSettings => isDemo ? _demoSettings! : snapshot.settings; Future _run(Future Function() operation) { final next = _queue.then((_) async { if (storageReady) { lastError = null; } try { await operation(); } catch (error) { lastError = error.toString(); DiagnosticLog.instance.add( 'operation_failed', fields: {'code': error.runtimeType.toString()}, ); } notifyListeners(); }); _queue = next; return next; } void _checkStorage() { if (!storageReady) throw StateError('请先恢复备份,避免覆盖损坏的保存文件'); } Future _save(AppSnapshot next) async { _checkStorage(); AppSnapshot.validateCourseSource(next.courses, next.schoolTerm); await store.save(next); snapshot = next; lastError = null; DiagnosticLog.instance.add( 'snapshot_saved', fields: {'count': next.courses.length, 'term': next.schoolTerm?.id}, ); } Future initialize({bool reconcileServices = true}) => _run(() async { try { snapshot = await store.load() ?? AppSnapshot(); storageReady = true; } catch (_) { storageReady = false; throw const FormatException('保存文件读取失败,可恢复最近备份'); } if (reconcileServices) await _reconcile(); }); Future restoreBackup() => _run(() async { snapshot = await store.restoreBackup(); storageReady = true; lastError = null; await _reconcile(); }); Future showDemo({List? courses}) => _run(() async { _demo = courses ?? demoCourses(); isDemo = true; final today = ScheduleEngine.schoolDate(clock()); _demoSettings = snapshot.settings.copyWith( semester: Semester( startDate: today.subtract(Duration(days: today.weekday - 1)), totalWeeks: 20, ), remindersEnabled: false, calendarEnabled: false, ); }); Future leaveDemo() => _run(() async { isDemo = false; }); Future selectSchool(SupportedSchool school) async { var saved = false; await _run(() async { await _save(snapshot.copyWith(selectedSchool: school)); saved = true; }); return saved; } Future updateSettings( AppSettings Function(AppSettings current) change, ) => _run(() async { final previous = snapshot.settings; final value = change(previous); if (value.reminderOffsets.isEmpty || value.reminderOffsets.any( (v) => !AppSettings.allowedReminderMinutes.contains(v), )) { throw ArgumentError('提醒时间无效'); } if (!value.courseHeightScale.isFinite || value.courseHeightScale < AppSettings.minCourseHeightScale || value.courseHeightScale > AppSettings.maxCourseHeightScale) { throw ArgumentError('课程条长度无效'); } await _save(snapshot.copyWith(settings: value)); if (isDemo) { _demoSettings = _demoSettings!.copyWith( colorMode: value.colorMode, singleColorArgb: value.singleColorArgb, courseHeightScale: value.courseHeightScale, ); } final scheduleChanged = previous.semester != value.semester || previous.remindersEnabled != value.remindersEnabled || !listEquals(previous.reminderOffsets, value.reminderOffsets) || previous.calendarEnabled != value.calendarEnabled || previous.calendarId != value.calendarId; if (scheduleChanged) await _reconcile(); }); Future applyImport( ImportDraft draft, { required bool confirmedEmpty, bool Function()? canApply, }) => _run(() async { if (canApply != null && !canApply()) throw StateError('学校页面已变化,请重新导入。'); if (draft.courses.isEmpty && (!draft.explicitlyEmpty || !confirmedEmpty)) { throw const FormatException('未检测到课程,已有课表保持不变'); } if (draft.courses.map((c) => c.id).toSet().length != draft.courses.length) { throw const FormatException('课程标识重复'); } if (draft.source == CourseSource.demo) { _demo = draft.courses; isDemo = true; final today = ScheduleEngine.schoolDate(clock()); _demoSettings = snapshot.settings.copyWith( semester: Semester( startDate: today.subtract(Duration(days: today.weekday - 1)), totalWeeks: 20, ), remindersEnabled: false, calendarEnabled: false, ); return; } if (draft.courses.any((c) => c.source != draft.source)) { throw const FormatException('课程来源不一致'); } if (draft.schoolTerm?.school != SupportedSchool.uestcUndergraduate && draft.courses.any((c) => c.endPeriod > 11)) { throw const FormatException('第12节需要本科课表来源'); } await _save( snapshot.copyWith( courses: draft.courses, schoolTerm: draft.schoolTerm, clearExams: draft.schoolTerm?.school != SupportedSchool.uestcUndergraduate && snapshot.examSchedule?.school != SupportedSchool.uestcUndergraduate && _differentTerm(draft.schoolTerm), settings: _differentTerm(draft.schoolTerm) ? snapshot.settings.copyWith(clearSemester: true) : snapshot.settings, importMetadata: ImportMetadata( source: draft.source, importedAt: clock(), parserVersion: draft.parserVersion, ), ), ); isDemo = false; await _reconcile(); }); Future clearCourses() => _run(() async { // Preserve ownership mapping until cleanup has actually succeeded. await _save( snapshot.copyWith( courses: [], clearMetadata: true, calendarCleanupPending: true, ), ); isDemo = false; try { await notifications.cancelOwned(); } catch (_) { reminderResult = const ReminderResult( ReminderStatus.error, message: '提醒清理失败,可重试', ); } await _cleanupCalendar(); }); Future applyExams(ExamSchedule value, {bool Function()? canApply}) => _run(() async { if (canApply != null && !canApply()) throw StateError('学校查询已取消,请重新读取。'); final validated = ExamSchedule.fromJson(value.toJson()); if (validated.school == SupportedSchool.uestcUndergraduate) { await _save(snapshot.copyWith(examSchedule: validated)); return; } final term = SchoolTerm(int.parse(validated.termId)); final changed = _differentTerm(term); await _save( snapshot.copyWith( examSchedule: validated, schoolTerm: term, courses: changed ? [] : null, clearMetadata: changed, settings: changed ? snapshot.settings.copyWith(clearSemester: true) : null, ), ); if (changed) { isDemo = false; await _reconcile(); } }); bool _differentTerm(SchoolTerm? next) => next != null && ((snapshot.schoolTerm != null && snapshot.schoolTerm != next) || (snapshot.schoolTerm == null && (snapshot.courses.isNotEmpty || snapshot.examSchedule != null))); Future clearExams() => _run(() async { await _save(snapshot.copyWith(clearExams: true)); }); Future disableCalendar({required bool deleteEvents}) => _run(() async { await _save( snapshot.copyWith( settings: snapshot.settings.copyWith(calendarEnabled: false), calendarCleanupPending: deleteEvents || snapshot.calendarCleanupPending, ), ); if (deleteEvents) { await _cleanupCalendar(); } calendarSyncError = null; }); Future enableCalendarFromUserAction(String calendarId) => _run(() async { if (isDemo) throw StateError('示例课表不会导入日历'); if (snapshot.settings.semester == null) throw StateError('请先设置学期'); if (!await calendar.requestAccess()) { calendarSyncError = '日历权限未获允许'; return; } if (snapshot.calendarCleanupPending) { await _cleanupCalendar(); } final calendars = await calendar.listWritableCalendars(); if (!calendars.any((c) => c.id == calendarId)) { calendarSyncError = '请选择可写入的日历'; return; } final settings = snapshot.settings.copyWith( calendarEnabled: true, calendarId: calendarId, ); // Keep the chosen calendar for recovery if the first native sync fails midway. await _save( snapshot.copyWith( settings: settings.copyWith(calendarEnabled: false), calendarCleanupPending: true, calendarEventIds: snapshot.settings.calendarId == calendarId ? snapshot.calendarEventIds : {}, ), ); try { final ids = await calendar.sync( ScheduleEngine.occurrences(snapshot.courses, settings.semester!), calendarId, snapshot.calendarEventIds, ); await _save( snapshot.copyWith( settings: settings, calendarEventIds: ids, calendarCleanupPending: false, ), ); calendarSyncError = null; } catch (_) { calendarSyncError = '日历同步失败,可重试'; } }); Future enableRemindersFromUserAction() => _run(() async { if (isDemo) throw StateError('示例课表不会生成提醒'); await notifications.requestPermissions(); await _save( snapshot.copyWith( settings: snapshot.settings.copyWith(remindersEnabled: true), ), ); await _reconcile(); }); Future refreshServices() => _run(_reconcile); Future startFreshData() => _run(() async { if (storageReady) { throw StateError('保存文件正常,无需重新建立'); } snapshot = await store.startFresh(); storageReady = true; isDemo = false; lastError = null; await _reconcile(); }); Future retryCalendarCleanup() => _run(() async { if (await calendar.requestAccess()) { await _cleanupCalendar(); } else { calendarSyncError = '日历权限未获允许,课程日程尚未清理'; } }); Future _cleanupCalendar() async { try { await calendar.deleteOwned(snapshot.calendarEventIds); final calendarId = snapshot.settings.calendarId; if (calendarId != null) { await calendar.deleteOwned(await calendar.ownedEventIds(calendarId)); } await _save( snapshot.copyWith(calendarEventIds: {}, calendarCleanupPending: false), ); calendarSyncError = null; } catch (_) { calendarSyncError = '课程日程尚未清理,请允许日历访问后重试'; rethrow; } } Future _reconcile() async { if (!storageReady) { return; } final settings = snapshot.settings; final occurrences = settings.semester == null ? [] : ScheduleEngine.occurrences(snapshot.courses, settings.semester!); try { reminderResult = await notifications.reconcile( occurrences, settings, clock(), ); reminderErrorCode = null; DiagnosticLog.instance.add( 'reminders_reconciled', fields: { 'count': occurrences.length, 'status': reminderResult.status.name, }, ); } catch (error) { reminderErrorCode = error is ReminderSetupException ? error.code : error is PlatformException ? error.code : error.runtimeType.toString(); DiagnosticLog.instance.add( 'reminders_failed', fields: {'code': error.runtimeType.toString()}, ); reminderResult = ReminderResult( ReminderStatus.error, message: error is ReminderSetupException ? error.message : '提醒设置未完成,请点击“检查提醒”后重试。', ); } if (snapshot.calendarCleanupPending) { try { await _cleanupCalendar(); } catch (_) { return; } } if (settings.calendarEnabled && settings.calendarId != null) { try { final ids = await calendar.sync( occurrences, settings.calendarId!, snapshot.calendarEventIds, ); await _save(snapshot.copyWith(calendarEventIds: ids)); calendarSyncError = null; } catch (_) { calendarSyncError = '日历同步失败,可重试'; } } } }