feat: deliver RSchedule service login exams and elastic glass navigation
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user