fix: preserve queued settings and recover interrupted or corrupt state

This commit is contained in:
Codex
2026-10-13 09:32:50 +08:00
parent 5ceab20b67
commit da5b8076a0
16 changed files with 373 additions and 50 deletions
+62 -38
View File
@@ -85,7 +85,10 @@ class AppController extends ChangeNotifier {
Future<void> leaveDemo() => _run(() async {
isDemo = false;
});
Future<void> updateSettings(AppSettings value) => _run(() async {
Future<void> updateSettings(
AppSettings Function(AppSettings current) change,
) => _run(() async {
final value = change(snapshot.settings);
if (![5, 10, 15, 30, 60].contains(value.reminderMinutes)) {
throw ArgumentError('提醒时间无效');
}
@@ -170,43 +173,54 @@ class AppController extends ChangeNotifier {
}
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,
);
// Keep the chosen calendar for recovery if the first native sync fails midway.
await _save(
snapshot.copyWith(settings: settings.copyWith(calendarEnabled: false)),
);
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> 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,
);
// Keep the chosen calendar for recovery if the first native sync fails midway.
await _save(
snapshot.copyWith(
settings: settings.copyWith(calendarEnabled: false),
calendarCleanupPending: true,
calendarEventIds: snapshot.settings.calendarId == calendarId
? snapshot.calendarEventIds
: {},
),
);
try {
final ids = await calendar.sync(
ScheduleEngine.occurrences(snapshot.courses, settings.semester!),
calendarId,
snapshot.calendarEventIds,
);
await _save(
snapshot.copyWith(
settings: settings,
calendarEventIds: ids,
calendarCleanupPending: false,
),
);
calendarSyncError = null;
} catch (_) {
calendarSyncError = '日历同步失败,可重试';
}
});
Future<void> enableRemindersFromUserAction() => _run(() async {
if (isDemo) throw StateError('示例课表不会生成提醒');
await notifications.requestPermissions();
@@ -218,6 +232,16 @@ class AppController extends ChangeNotifier {
await _reconcile();
});
Future<void> refreshServices() => _run(_reconcile);
Future<void> startFreshData() => _run(() async {
if (storageReady) {
throw StateError('保存文件正常,无需重新建立');
}
snapshot = await store.startFresh();
storageReady = true;
isDemo = false;
lastError = null;
await _reconcile();
});
Future<void> retryCalendarCleanup() => _run(() async {
if (await calendar.requestAccess()) {
await _cleanupCalendar();
+30
View File
@@ -159,6 +159,31 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
}
}
Future<void> _startFresh() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('重新建立课表'),
content: const Text(
'损坏文件会保留为恢复副本,然后重新建立空课表。已有课程通知会取消;手机日历中原有课程日程需手动处理。',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('保留损坏文件并重新建立'),
),
],
),
);
if (confirmed == true) {
await widget.controller.startFreshData();
}
}
@override
Widget build(BuildContext context) => AnimatedBuilder(
animation: Listenable.merge([
@@ -198,6 +223,11 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
onPressed: widget.controller.restoreBackup,
child: const Text('恢复最近备份'),
),
if (!widget.controller.storageReady)
TextButton(
onPressed: _startFresh,
child: const Text('重新建立课表'),
),
],
),
),
+14
View File
@@ -46,4 +46,18 @@ class FileSnapshotStore extends SnapshotStore {
await temporary.rename(_file.path);
return previous;
}
@override
Future<AppSnapshot> startFresh() async {
await directory.create(recursive: true);
final stamp = DateTime.now().toUtc().microsecondsSinceEpoch;
for (final file in [_file, _backup]) {
if (await file.exists()) {
await file.rename('${file.path}.damaged-$stamp');
}
}
final fresh = AppSnapshot();
await save(fresh);
return fresh;
}
}
+1
View File
@@ -4,4 +4,5 @@ abstract class SnapshotStore {
Future<AppSnapshot?> load();
Future<void> save(AppSnapshot snapshot);
Future<AppSnapshot> restoreBackup() => Future.error(StateError('没有可恢复的备份'));
Future<AppSnapshot> startFresh() => Future.error(StateError('无法重新建立保存文件'));
}
+11 -6
View File
@@ -138,14 +138,16 @@ class _SettingsPageState extends State<SettingsPage> {
label: const Text('多彩'),
selected: s.colorMode == ColorMode.multicolor,
onSelected: (_) => c.updateSettings(
s.copyWith(colorMode: ColorMode.multicolor),
(current) =>
current.copyWith(colorMode: ColorMode.multicolor),
),
),
ChoiceChip(
label: const Text('单色'),
selected: s.colorMode == ColorMode.single,
onSelected: (_) => c.updateSettings(
s.copyWith(colorMode: ColorMode.single),
(current) =>
current.copyWith(colorMode: ColorMode.single),
),
),
],
@@ -166,7 +168,7 @@ class _SettingsPageState extends State<SettingsPage> {
button: true,
child: InkWell(
onTap: () => c.updateSettings(
s.copyWith(
(current) => current.copyWith(
singleColorArgb: color,
colorMode: ColorMode.single,
),
@@ -217,7 +219,8 @@ class _SettingsPageState extends State<SettingsPage> {
await c.enableRemindersFromUserAction();
} else {
await c.updateSettings(
s.copyWith(remindersEnabled: false),
(current) =>
current.copyWith(remindersEnabled: false),
);
}
},
@@ -234,7 +237,9 @@ class _SettingsPageState extends State<SettingsPage> {
onSelected: c.isDemo
? null
: (_) => c.updateSettings(
s.copyWith(reminderMinutes: minutes),
(current) => current.copyWith(
reminderMinutes: minutes,
),
),
),
],
@@ -361,7 +366,7 @@ class _SettingsPageState extends State<SettingsPage> {
});
if (_weeksError != null || _dateError != null) return;
await c.updateSettings(
s.copyWith(
(current) => current.copyWith(
semester: Semester(
startDate: _start!,
totalWeeks: weeks!,