import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import '../domain/schedule_engine.dart'; import '../features/feedback/feedback_http_sender.dart'; import '../features/feedback/feedback_submission.dart'; 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'; class HepanApp extends StatelessWidget { const HepanApp({ super.key, required this.controller, this.schoolBuilder, this.school, this.notificationSelection, this.feedbackSender, }); final AppController controller; final Widget Function()? schoolBuilder; final SchoolSession? school; final ValueNotifier? notificationSelection; final FeedbackSender? feedbackSender; @override Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, title: 'TPSchedule', 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, feedbackSender: feedbackSender ?? configuredFeedbackSender(), ), ); } class _Shell extends StatefulWidget { const _Shell({ required this.controller, this.schoolBuilder, this.school, this.notificationSelection, required this.feedbackSender, }); final AppController controller; final Widget Function()? schoolBuilder; final SchoolSession? school; final ValueNotifier? notificationSelection; final FeedbackSender feedbackSender; @override State<_Shell> createState() => _ShellState(); } class _ShellState extends State<_Shell> with WidgetsBindingObserver { int _tab = 0; void _selectTab(int index) { if (index == _tab) return; setState(() => _tab = index); } @override 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 _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) { widget.controller.refreshServices(); } } void _school({bool exams = false, bool chooseTerm = false}) { if (widget.schoolBuilder != null) { final term = widget.controller.snapshot.schoolTerm; if (exams) { widget.school?.openExams( term: term, chooseTerm: chooseTerm || term == null, ); } else { widget.school?.openTimetable( term: term, chooseTerm: chooseTerm || term == null, ); } Navigator.push( context, MaterialPageRoute(builder: (_) => widget.schoolBuilder!()), ); } else { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('学校认证'), content: const Text('学校认证入口正在接入。你可以先体验示例课表或导入课表文件。'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('知道了'), ), ], ), ); } } Future _logout() async { final choice = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('退出学校登录'), content: const Text('已导入的课表和考试信息是否保留在手机中?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, 0), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, 1), child: const Text('保留课表和考试'), ), TextButton( onPressed: () => Navigator.pop(context, 2), child: const Text('清除课表和考试'), ), ], ), ); if (choice == null || choice == 0) return; await widget.school?.logout(); if (choice == 2) { await widget.controller.clearCourses(); await widget.controller.clearExams(); } } Future _startFresh() async { final confirmed = await showDialog( 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([ widget.controller, if (widget.school != null) widget.school!, ]), builder: (context, _) => Scaffold( body: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFF0F6FF), Color(0xFFF7FAFF), Color(0xFFEDF4FF)], ), ), child: SafeArea( bottom: false, child: Stack( children: [ Positioned.fill( child: Column( children: [ if (widget.controller.lastError != null) Container( width: double.infinity, color: const Color(0xFFF8ECE6), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.controller.lastError!, style: const TextStyle(fontSize: 12), ), if (!widget.controller.storageReady) TextButton( onPressed: widget.controller.restoreBackup, child: const Text('恢复最近备份'), ), if (!widget.controller.storageReady) TextButton( onPressed: _startFresh, child: const Text('重新建立课表'), ), ], ), ), Expanded( child: TabDeck( index: _tab, children: [ TimetablePage( controller: widget.controller, onSync: _school, ), ExamsPage( controller: widget.controller, onSync: () { _school(exams: true); }, ), ProfilePage( controller: widget.controller, feedbackSender: widget.feedbackSender, onLogin: _school, onUpdateTerm: () => _school(chooseTerm: true), authStatus: widget.school?.statusLabel ?? '未登录', logoutLabel: widget.school?.logoutFailed == true ? '重试退出学校登录' : '退出学校登录', onLogout: widget.school?.canLogout == true ? _logout : null, ), ], ), ), ], ), ), Positioned( left: 0, right: 0, bottom: 16 + MediaQuery.paddingOf(context).bottom, child: Center( child: GlassDock( key: const Key('floating-dock'), selectedIndex: _tab, onSelected: _selectTab, ), ), ), ], ), ), ), ), ); }