feat: persist timetable and validate confirmed imports
This commit is contained in:
@@ -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,
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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('课表文件格式无效');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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('没有可恢复的备份'));
|
||||
}
|
||||
Reference in New Issue
Block a user