189 lines
6.9 KiB
Dart
189 lines
6.9 KiB
Dart
import 'app_settings.dart';
|
|
import 'course.dart';
|
|
import 'semester.dart';
|
|
import 'exam_schedule.dart';
|
|
import 'school_term.dart';
|
|
|
|
class ImportMetadata {
|
|
const ImportMetadata({
|
|
required this.source,
|
|
required this.importedAt,
|
|
required this.parserVersion,
|
|
});
|
|
final CourseSource source;
|
|
final DateTime importedAt;
|
|
final String parserVersion;
|
|
}
|
|
|
|
class AppSnapshot {
|
|
AppSnapshot({
|
|
AppSettings? settings,
|
|
List<Course> courses = const [],
|
|
this.importMetadata,
|
|
this.examSchedule,
|
|
SchoolTerm? schoolTerm,
|
|
this.calendarCleanupPending = false,
|
|
Map<String, String> calendarEventIds = const {},
|
|
}) : settings = settings ?? AppSettings(),
|
|
schoolTerm = schoolTerm ?? _legacyTerm(courses, examSchedule),
|
|
courses = List.unmodifiable(courses),
|
|
calendarEventIds = Map.unmodifiable(calendarEventIds);
|
|
final AppSettings settings;
|
|
final List<Course> courses;
|
|
final ImportMetadata? importMetadata;
|
|
final ExamSchedule? examSchedule;
|
|
final SchoolTerm? schoolTerm;
|
|
final Map<String, String> calendarEventIds;
|
|
final bool calendarCleanupPending;
|
|
static SchoolTerm? _legacyTerm(List<Course> courses, ExamSchedule? exams) {
|
|
final ids = courses
|
|
.map(
|
|
(course) => RegExp(r'^uestc-(\d+)-').firstMatch(course.id)?.group(1),
|
|
)
|
|
.whereType<String>()
|
|
.toSet();
|
|
if (exams != null) ids.add(exams.termId);
|
|
if (ids.length != 1) return null;
|
|
final id = int.tryParse(ids.single);
|
|
return id != null && id >= 1 && id <= 9999 ? SchoolTerm(id) : null;
|
|
}
|
|
|
|
AppSnapshot copyWith({
|
|
AppSettings? settings,
|
|
List<Course>? courses,
|
|
ImportMetadata? importMetadata,
|
|
Map<String, String>? calendarEventIds,
|
|
bool clearMetadata = false,
|
|
ExamSchedule? examSchedule,
|
|
bool clearExams = false,
|
|
SchoolTerm? schoolTerm,
|
|
bool? calendarCleanupPending,
|
|
}) => AppSnapshot(
|
|
settings: settings ?? this.settings,
|
|
courses: courses ?? this.courses,
|
|
importMetadata: clearMetadata
|
|
? null
|
|
: importMetadata ?? this.importMetadata,
|
|
examSchedule: clearExams ? null : examSchedule ?? this.examSchedule,
|
|
schoolTerm: schoolTerm ?? this.schoolTerm,
|
|
calendarEventIds: calendarEventIds ?? this.calendarEventIds,
|
|
calendarCleanupPending:
|
|
calendarCleanupPending ?? this.calendarCleanupPending,
|
|
);
|
|
Map<String, dynamic> toJson() => {
|
|
'schemaVersion': 1,
|
|
'examSchedule': examSchedule?.toJson(),
|
|
'schoolTerm': schoolTerm?.toJson(),
|
|
'courses': courses.map((c) => c.toJson()).toList(),
|
|
'settings': {
|
|
'semester': settings.semester == null
|
|
? null
|
|
: {
|
|
'startDate': settings.semester!.startDate
|
|
.toIso8601String()
|
|
.substring(0, 10),
|
|
'totalWeeks': settings.semester!.totalWeeks,
|
|
},
|
|
'remindersEnabled': settings.remindersEnabled,
|
|
'reminderMinutes': settings.reminderMinutes,
|
|
'reminderOffsets': settings.reminderOffsets,
|
|
'colorMode': settings.colorMode.name,
|
|
'singleColorArgb': settings.singleColorArgb,
|
|
'calendarEnabled': settings.calendarEnabled,
|
|
'calendarId': settings.calendarId,
|
|
'courseHeightScale': settings.courseHeightScale,
|
|
},
|
|
'importMetadata': importMetadata == null
|
|
? null
|
|
: {
|
|
'source': importMetadata!.source.name,
|
|
'importedAt': importMetadata!.importedAt.toUtc().toIso8601String(),
|
|
'parserVersion': importMetadata!.parserVersion,
|
|
},
|
|
'calendarEventIds': calendarEventIds,
|
|
'calendarCleanupPending': calendarCleanupPending,
|
|
};
|
|
static AppSnapshot fromJson(Map<String, dynamic> json) {
|
|
try {
|
|
if (json['schemaVersion'] != 1) throw const FormatException('不支持的保存版本');
|
|
final values = (json['courses'] as List)
|
|
.map((c) => Course.fromJson(Map<String, dynamic>.from(c as Map)))
|
|
.toList();
|
|
if (values.map((c) => c.id).toSet().length != values.length ||
|
|
values.any((c) => c.source == CourseSource.demo)) {
|
|
throw const FormatException('保存课表无效');
|
|
}
|
|
final s = Map<String, dynamic>.from(json['settings'] as Map);
|
|
final rawOffsets = s.containsKey('reminderOffsets')
|
|
? s['reminderOffsets']
|
|
: [s['reminderMinutes']];
|
|
if (rawOffsets is! List ||
|
|
rawOffsets.isEmpty ||
|
|
rawOffsets.any(
|
|
(v) => v is! int || !AppSettings.allowedReminderMinutes.contains(v),
|
|
)) {
|
|
throw const FormatException('提醒时间无效');
|
|
}
|
|
final offsets = rawOffsets.cast<int>();
|
|
final color = s['singleColorArgb'] as int;
|
|
if (color < 0 || color > 0xFFFFFFFF) throw const FormatException('颜色无效');
|
|
final rawHeight = s.containsKey('courseHeightScale')
|
|
? s['courseHeightScale']
|
|
: AppSettings.defaultCourseHeightScale;
|
|
if (rawHeight is! num ||
|
|
!rawHeight.isFinite ||
|
|
rawHeight < AppSettings.minCourseHeightScale ||
|
|
rawHeight > AppSettings.maxCourseHeightScale) {
|
|
throw const FormatException('课程条长度无效');
|
|
}
|
|
final sem = s['semester'];
|
|
final term = sem == null
|
|
? null
|
|
: Semester(
|
|
startDate: DateTime.parse(sem['startDate'] as String),
|
|
totalWeeks: sem['totalWeeks'] as int,
|
|
);
|
|
final metadata = json['importMetadata'];
|
|
return AppSnapshot(
|
|
schoolTerm: json['schoolTerm'] == null
|
|
? null
|
|
: SchoolTerm.fromJson(
|
|
Map<String, dynamic>.from(json['schoolTerm'] as Map),
|
|
),
|
|
examSchedule: json['examSchedule'] == null
|
|
? null
|
|
: ExamSchedule.fromJson(
|
|
Map<String, dynamic>.from(json['examSchedule'] as Map),
|
|
),
|
|
calendarCleanupPending:
|
|
(json['calendarCleanupPending'] ?? false) as bool,
|
|
courses: values,
|
|
settings: AppSettings(
|
|
semester: term,
|
|
remindersEnabled: s['remindersEnabled'] as bool,
|
|
reminderOffsets: offsets,
|
|
colorMode: ColorMode.values.byName(s['colorMode'] as String),
|
|
singleColorArgb: color,
|
|
calendarEnabled: s['calendarEnabled'] as bool,
|
|
calendarId: s['calendarId'] as String?,
|
|
courseHeightScale: rawHeight.toDouble(),
|
|
),
|
|
importMetadata: metadata == null
|
|
? null
|
|
: ImportMetadata(
|
|
source: CourseSource.values.byName(
|
|
metadata['source'] as String,
|
|
),
|
|
importedAt: DateTime.parse(metadata['importedAt'] as String),
|
|
parserVersion: metadata['parserVersion'] as String,
|
|
),
|
|
calendarEventIds: (json['calendarEventIds'] as Map).map(
|
|
(k, v) => MapEntry(k as String, v as String),
|
|
),
|
|
);
|
|
} catch (_) {
|
|
throw const FormatException('保存文件内容无效,未覆盖已有数据');
|
|
}
|
|
}
|
|
}
|