Files
TPSchedule/mobile/lib/app/app_controller.dart
T

359 lines
12 KiB
Dart

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';
import '../domain/exam_schedule.dart';
import '../domain/school_term.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<Course> _demo = [];
AppSettings? _demoSettings;
Future<void> _queue = Future.value();
String? lastError, calendarSyncError;
ReminderResult reminderResult = const ReminderResult(ReminderStatus.off);
List<Course> get visibleCourses => isDemo ? _demo : snapshot.courses;
AppSettings get visibleSettings =>
isDemo ? _demoSettings! : snapshot.settings;
Future<void> _run(Future<void> 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<void> _save(AppSnapshot next) async {
_checkStorage();
await store.save(next);
snapshot = next;
lastError = null;
}
Future<void> initialize() => _run(() async {
try {
snapshot = await store.load() ?? AppSnapshot();
storageReady = true;
} catch (_) {
storageReady = false;
throw const FormatException('保存文件读取失败,可恢复最近备份');
}
await _reconcile();
});
Future<void> restoreBackup() => _run(() async {
snapshot = await store.restoreBackup();
storageReady = true;
lastError = null;
await _reconcile();
});
Future<void> showDemo({List<Course>? 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<void> leaveDemo() => _run(() async {
isDemo = false;
});
Future<void> 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<void> 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,
schoolTerm: draft.schoolTerm,
clearExams: _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<void> 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<void> applyExams(ExamSchedule value) => _run(() async {
final validated = ExamSchedule.fromJson(value.toJson());
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<void> clearExams() => _run(() async {
await _save(snapshot.copyWith(clearExams: true));
});
Future<void> 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<void> 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<void> enableRemindersFromUserAction() => _run(() async {
if (isDemo) throw StateError('示例课表不会生成提醒');
await notifications.requestPermissions();
await _save(
snapshot.copyWith(
settings: snapshot.settings.copyWith(remindersEnabled: true),
),
);
await _reconcile();
});
Future<void> refreshServices() => _run(_reconcile);
Future<void> startFreshData() => _run(() async {
if (storageReady) {
throw StateError('保存文件正常,无需重新建立');
}
snapshot = await store.startFresh();
storageReady = true;
isDemo = false;
lastError = null;
await _reconcile();
});
Future<void> retryCalendarCleanup() => _run(() async {
if (await calendar.requestAccess()) {
await _cleanupCalendar();
} else {
calendarSyncError = '日历权限未获允许,课程日程尚未清理';
}
});
Future<void> _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<void> _reconcile() async {
if (!storageReady) {
return;
}
final settings = snapshot.settings;
final occurrences = settings.semester == null
? <CourseOccurrence>[]
: 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 = '日历同步失败,可重试';
}
}
}
}