import 'package:flutter/foundation.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/calendar_service.dart'; import '../domain/schedule_engine.dart'; import '../data/demo_schedule.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; 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(); } notifyListeners(); }); _queue = next; return next; } void _checkStorage() { if (!storageReady) throw StateError('请先恢复备份,避免覆盖损坏的保存文件'); } Future _save(AppSnapshot next) async { _checkStorage(); await store.save(next); snapshot = next; lastError = null; } Future initialize() => _run(() async { try { snapshot = await store.load() ?? AppSnapshot(); storageReady = true; } catch (_) { storageReady = false; throw const FormatException('保存文件读取失败,可恢复最近备份'); } 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 updateSettings( AppSettings Function(AppSettings current) change, ) => _run(() async { final previous = snapshot.settings; final value = change(previous); if (![5, 10, 15, 30, 60].contains(value.reminderMinutes)) { 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 || previous.reminderMinutes != value.reminderMinutes || previous.calendarEnabled != value.calendarEnabled || previous.calendarId != value.calendarId; if (scheduleChanged) await _reconcile(); }); Future applyImport( ImportDraft draft, { required bool confirmedEmpty, }) => _run(() async { 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('课程来源不一致'); } await _save( snapshot.copyWith( courses: draft.courses, 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 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(), ); } catch (_) { reminderResult = const ReminderResult( ReminderStatus.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 = '日历同步失败,可重试'; } } } }