feat: deliver RSchedule service login exams and elastic glass navigation

This commit is contained in:
Codex
2026-10-13 17:06:50 +08:00
parent 9193fd8e43
commit 8f20a341c4
35 changed files with 1742 additions and 453 deletions
+8
View File
@@ -8,6 +8,7 @@ import '../services/notification_service.dart';
import '../services/calendar_service.dart';
import '../domain/schedule_engine.dart';
import '../data/demo_schedule.dart';
import '../domain/exam_schedule.dart';
class AppController extends ChangeNotifier {
AppController(this.store, this.notifications, this.calendar, this.clock);
@@ -174,6 +175,13 @@ class AppController extends ChangeNotifier {
}
await _cleanupCalendar();
});
Future<void> applyExams(ExamSchedule value) => _run(() async {
final validated = ExamSchedule.fromJson(value.toJson());
await _save(snapshot.copyWith(examSchedule: validated));
});
Future<void> clearExams() => _run(() async {
await _save(snapshot.copyWith(clearExams: true));
});
Future<void> disableCalendar({required bool deleteEvents}) => _run(() async {
await _save(
snapshot.copyWith(
+88 -21
View File
@@ -7,6 +7,8 @@ import '../features/timetable/course_details.dart';
import 'app_controller.dart';
import 'app_theme.dart';
import '../widgets/glass_dock.dart';
import '../widgets/tab_deck.dart';
import '../features/exams/exams_page.dart';
import '../features/timetable/timetable_page.dart';
import '../features/profile/profile_page.dart';
import '../features/school/school_session.dart';
@@ -26,7 +28,7 @@ class HepanApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Riverside Schedule',
title: 'RSchedule',
theme: hepanTheme(),
locale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN')],
@@ -57,6 +59,33 @@ class _Shell extends StatefulWidget {
class _ShellState extends State<_Shell> with WidgetsBindingObserver {
int _tab = 0;
bool _dockHidden = false;
double _scrollTravel = 0;
void _selectTab(int index) {
if (index == _tab) return;
setState(() {
_tab = index;
_dockHidden = false;
_scrollTravel = 0;
});
}
bool _scroll(ScrollNotification notification) {
if (_tab == 2 || notification.metrics.axis != Axis.vertical) return false;
if (notification is ScrollUpdateNotification &&
notification.dragDetails != null) {
final delta = notification.scrollDelta ?? 0;
if (delta.sign != _scrollTravel.sign) _scrollTravel = 0;
_scrollTravel += delta;
final hide = notification.metrics.pixels > 0 && _scrollTravel > 24;
final reveal = notification.metrics.pixels <= 0 || _scrollTravel < -16;
if ((hide && !_dockHidden) || (reveal && _dockHidden)) {
setState(() => _dockHidden = hide);
}
}
return false;
}
@override
void initState() {
super.initState();
@@ -107,8 +136,13 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
}
}
void _school() {
void _school({bool exams = false}) {
if (widget.schoolBuilder != null) {
if (exams) {
widget.school?.openExams();
} else {
widget.school?.openTimetable();
}
Navigator.push(
context,
MaterialPageRoute<void>(builder: (_) => widget.schoolBuilder!()),
@@ -135,7 +169,7 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
context: context,
builder: (context) => AlertDialog(
title: const Text('退出学校登录'),
content: const Text('已导入的课表是否保留在手机中?'),
content: const Text('已导入的课表和考试信息是否保留在手机中?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, 0),
@@ -143,11 +177,11 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
),
TextButton(
onPressed: () => Navigator.pop(context, 1),
child: const Text('保留课表'),
child: const Text('保留课表和考试'),
),
TextButton(
onPressed: () => Navigator.pop(context, 2),
child: const Text('清除课表'),
child: const Text('清除课表和考试'),
),
],
),
@@ -156,6 +190,7 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
await widget.school?.logout();
if (choice == 2) {
await widget.controller.clearCourses();
await widget.controller.clearExams();
}
}
@@ -232,19 +267,31 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
),
),
Expanded(
child: IndexedStack(
index: _tab,
children: [
TimetablePage(controller: widget.controller),
ProfilePage(
controller: widget.controller,
onLogin: _school,
authStatus: widget.school?.statusLabel ?? '未登录',
onLogout: widget.school?.hasOpened == true
? _logout
: null,
),
],
child: NotificationListener<ScrollNotification>(
onNotification: _scroll,
child: TabDeck(
index: _tab,
children: [
TimetablePage(controller: widget.controller),
ExamsPage(
controller: widget.controller,
onSync: () {
_school(exams: true);
},
),
ProfilePage(
controller: widget.controller,
onLogin: _school,
authStatus: widget.school?.statusLabel ?? '未登录',
logoutLabel: widget.school?.logoutFailed == true
? '重试退出学校登录'
: '退出学校登录',
onLogout: widget.school?.canLogout == true
? _logout
: null,
),
],
),
),
),
],
@@ -255,9 +302,29 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
right: 0,
bottom: 16 + MediaQuery.paddingOf(context).bottom,
child: Center(
child: GlassDock(
selectedIndex: _tab,
onSelected: (index) => setState(() => _tab = index),
child: IgnorePointer(
ignoring: _dockHidden,
child: ExcludeSemantics(
excluding: _dockHidden,
child: AnimatedSlide(
key: const Key('floating-dock'),
offset: _dockHidden
? const Offset(0, 1.8)
: Offset.zero,
duration: MediaQuery.disableAnimationsOf(context)
? Duration.zero
: const Duration(milliseconds: 240),
curve: Curves.easeOutCubic,
child: AnimatedOpacity(
opacity: _dockHidden ? 0 : 1,
duration: const Duration(milliseconds: 180),
child: GlassDock(
selectedIndex: _tab,
onSelected: _selectTab,
),
),
),
),
),
),
),