feat: persist timetable and validate confirmed imports

This commit is contained in:
Codex
2026-10-12 23:18:51 +08:00
parent ae8e347e49
commit 808af007a2
13 changed files with 852 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import '../domain/course.dart';
List<Course> demoCourses() => [
_demo('english', '学术英语', '示例 · 张老师', '示例 · 综合楼 A301', 1, 1, 2),
_demo('matrix', '矩阵论', '示例 · 李老师', '示例 · 品学楼 B205', 1, 5, 6),
_demo('research', '研究方法', '示例 · 陈老师', '示例 · 立人楼 A208', 2, 3, 4),
_demo('signal', '现代信号处理', '示例 · 王老师', '示例 · 品学楼 C302', 3, 1, 2),
_demo('seminar', '学术研讨', '示例 · 刘老师', '示例 · 实验室 501', 3, 9, 11),
_demo('science', '科技论文写作', '示例 · 周老师', '示例 · 综合楼 B102', 4, 5, 6),
_demo('experiment', '实验与实践', '示例 · 赵老师', '示例 · 实验楼 302', 5, 3, 4),
_demo('weekend', '专题报告', '示例 · 导师组', '示例 · 学术报告厅', 7, 9, 11),
];
Course _demo(
String id,
String name,
String teacher,
String room,
int day,
int first,
int last,
) => Course(
id: id,
name: name,
teacher: teacher,
room: room,
weekday: day,
startPeriod: first,
endPeriod: last,
weeks: Set.from(List.generate(20, (i) => i + 1)),
source: CourseSource.demo,
);
+49
View File
@@ -0,0 +1,49 @@
import 'dart:io';
import 'dart:convert';
import 'snapshot_store.dart';
import '../domain/app_snapshot.dart';
class FileSnapshotStore extends SnapshotStore {
FileSnapshotStore(this.directory);
final Directory directory;
File get _file => File('${directory.path}/hepan_snapshot.json');
File get _backup => File('${directory.path}/hepan_snapshot.bak');
Future<AppSnapshot> _read(File file) async {
final value = jsonDecode(await file.readAsString());
if (value is! Map<String, dynamic>) throw const FormatException('保存文件损坏');
return AppSnapshot.fromJson(value);
}
@override
Future<AppSnapshot?> load() async =>
await _file.exists() ? await _read(_file) : null;
@override
Future<void> save(AppSnapshot snapshot) async {
await directory.create(recursive: true);
final text = jsonEncode(snapshot.toJson());
// Validate before touching the last successfully saved version.
AppSnapshot.fromJson(jsonDecode(text) as Map<String, dynamic>);
if (await _file.exists()) {
try {
await _read(_file);
await _file.copy(_backup.path);
} on FormatException {
/* Keep an existing valid backup. */
}
}
final temporary = File('${_file.path}.tmp');
await temporary.writeAsString(text, flush: true);
await temporary.rename(_file.path);
}
@override
Future<AppSnapshot> restoreBackup() async {
if (!await _backup.exists()) throw StateError('没有可恢复的备份');
final previous = await _read(_backup);
final temporary = File('${_file.path}.tmp');
await temporary.writeAsString(jsonEncode(previous.toJson()), flush: true);
await temporary.rename(_file.path);
return previous;
}
}
+57
View File
@@ -0,0 +1,57 @@
import '../domain/course.dart';
import 'dart:convert';
class ImportDraft {
const ImportDraft({
required this.courses,
required this.explicitlyEmpty,
required this.source,
required this.parserVersion,
});
final List<Course> courses;
final bool explicitlyEmpty;
final CourseSource source;
final String parserVersion;
}
class JsonScheduleImport {
static ImportDraft parse(String text) {
try {
if (utf8.encode(text).length > 2 * 1024 * 1024)
throw const FormatException('文件不能超过 2 MB');
final json = jsonDecode(text);
if (json is! Map ||
json['schemaVersion'] != 1 ||
json['courses'] is! List ||
(json['isDemo'] != null && json['isDemo'] is! bool) ||
(json['explicitlyEmpty'] != null &&
json['explicitlyEmpty'] is! bool)) {
throw const FormatException('请选择版本 1 的课表 JSON 文件');
}
final source = json['isDemo'] == true
? CourseSource.demo
: CourseSource.file;
final courses = (json['courses'] as List)
.map(
(c) => Course.fromJson(
Map<String, dynamic>.from(c as Map),
source: source,
),
)
.toList();
if (courses.map((c) => c.id).toSet().length != courses.length)
throw const FormatException('课程标识重复');
return ImportDraft(
courses: List.unmodifiable(courses),
explicitlyEmpty: json['explicitlyEmpty'] == true,
source: source,
parserVersion: 'json-v1',
);
} on FormatException {
rethrow;
} catch (_) {
throw const FormatException('课表文件格式无效');
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import '../domain/app_snapshot.dart';
abstract class SnapshotStore {
Future<AppSnapshot?> load();
Future<void> save(AppSnapshot snapshot);
Future<AppSnapshot> restoreBackup() => Future.error(StateError('没有可恢复的备份'));
}