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

238 lines
7.6 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));
isDemo = false;
await notifications.cancelOwned();
await calendar.deleteOwned(snapshot.calendarEventIds);
await _save(snapshot.copyWith(calendarEventIds: {}));
});
Future<void> disableCalendar({required bool deleteEvents}) => _run(() async {
await _save(
snapshot.copyWith(
settings: snapshot.settings.copyWith(calendarEnabled: false),
),
);
if (deleteEvents) {
await calendar.deleteOwned(snapshot.calendarEventIds);
await _save(snapshot.copyWith(calendarEventIds: {}));
}
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;
}
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> _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 (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 = '日历同步失败,可重试';
}
}
}
}