280 lines
8.5 KiB
Dart
280 lines
8.5 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';
|
|
|
|
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 value) => _run(() async {
|
|
if (![5, 10, 15, 30, 60].contains(value.reminderMinutes)) {
|
|
throw ArgumentError('提醒时间无效');
|
|
}
|
|
await _save(snapshot.copyWith(settings: value));
|
|
if (isDemo) {
|
|
_demoSettings = _demoSettings!.copyWith(
|
|
colorMode: value.colorMode,
|
|
singleColorArgb: value.singleColorArgb,
|
|
);
|
|
}
|
|
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,
|
|
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> 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,
|
|
);
|
|
try {
|
|
final ids = await calendar.sync(
|
|
ScheduleEngine.occurrences(snapshot.courses, settings.semester!),
|
|
calendarId,
|
|
snapshot.calendarEventIds,
|
|
);
|
|
await _save(
|
|
snapshot.copyWith(settings: settings, calendarEventIds: ids),
|
|
);
|
|
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> retryCalendarCleanup() => _run(() async {
|
|
if (await calendar.requestAccess()) {
|
|
await _cleanupCalendar();
|
|
} else {
|
|
calendarSyncError = '日历权限未获允许,课程日程尚未清理';
|
|
}
|
|
});
|
|
Future<void> _cleanupCalendar() async {
|
|
try {
|
|
await calendar.deleteOwned(snapshot.calendarEventIds);
|
|
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 = '日历同步失败,可重试';
|
|
}
|
|
}
|
|
}
|
|
}
|