feat: compose Android app and verify native calendar and reminders

This commit is contained in:
Codex
2026-10-13 09:14:47 +08:00
parent 2c4256b880
commit 5ceab20b67
37 changed files with 695 additions and 229 deletions
+41 -34
View File
@@ -170,40 +170,43 @@ 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,
);
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)),
);
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();
@@ -225,6 +228,10 @@ class AppController extends ChangeNotifier {
Future<void> _cleanupCalendar() async {
try {
await calendar.deleteOwned(snapshot.calendarEventIds);
final calendarId = snapshot.settings.calendarId;
if (calendarId != null) {
await calendar.deleteOwned(await calendar.ownedEventIds(calendarId));
}
await _save(
snapshot.copyWith(calendarEventIds: {}, calendarCleanupPending: false),
);
+48 -1
View File
@@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../domain/schedule_engine.dart';
import '../features/timetable/course_details.dart';
import 'app_controller.dart';
import 'app_theme.dart';
@@ -13,28 +17,40 @@ class HepanApp extends StatelessWidget {
required this.controller,
this.schoolBuilder,
this.school,
this.notificationSelection,
});
final AppController controller;
final Widget Function()? schoolBuilder;
final SchoolSession? school;
final ValueNotifier<String?>? notificationSelection;
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
title: '河畔Everyday',
theme: hepanTheme(),
locale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN')],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
home: _Shell(
controller: controller,
schoolBuilder: schoolBuilder,
school: school,
notificationSelection: notificationSelection,
),
);
}
class _Shell extends StatefulWidget {
const _Shell({required this.controller, this.schoolBuilder, this.school});
const _Shell({
required this.controller,
this.schoolBuilder,
this.school,
this.notificationSelection,
});
final AppController controller;
final Widget Function()? schoolBuilder;
final SchoolSession? school;
final ValueNotifier<String?>? notificationSelection;
@override
State<_Shell> createState() => _ShellState();
}
@@ -45,14 +61,45 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
widget.notificationSelection?.addListener(_selectNotification);
WidgetsBinding.instance.addPostFrameCallback((_) => _selectNotification());
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
widget.notificationSelection?.removeListener(_selectNotification);
super.dispose();
}
Future<void> _selectNotification() async {
final key = widget.notificationSelection?.value;
if (key == null || !mounted) {
return;
}
widget.notificationSelection!.value = null;
await widget.controller.leaveDemo();
if (!mounted) {
return;
}
Navigator.of(context).popUntil((route) => route.isFirst);
setState(() => _tab = 0);
final semester = widget.controller.snapshot.settings.semester;
if (semester == null) {
return;
}
final events = ScheduleEngine.occurrences(
widget.controller.snapshot.courses,
semester,
).where((e) => e.key == key);
if (events.isNotEmpty) {
showCourseDetails(context, events.first);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('这门课程已更新,请查看当前课表')));
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {