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'; import '../widgets/glass_dock.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, }); final AppController controller; final Widget Function()? schoolBuilder; final SchoolSession? school; final ValueNotifier? 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, this.notificationSelection, }); final AppController controller; final Widget Function()? schoolBuilder; final SchoolSession? school; final ValueNotifier? notificationSelection; @override State<_Shell> createState() => _ShellState(); } class _ShellState extends State<_Shell> with WidgetsBindingObserver { int _tab = 0; @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() { if (widget.schoolBuilder != 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(); } } @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(0xFFF0F7F5), Color(0xFFF7F8F5), Color(0xFFEFF5F3)], ), ), 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('恢复最近备份'), ), ], ), ), 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, ), ], ), ), ], ), ), Positioned( left: 0, right: 0, bottom: 16 + MediaQuery.paddingOf(context).bottom, child: Center( child: GlassDock( selectedIndex: _tab, onSelected: (index) => setState(() => _tab = index), ), ), ), ], ), ), ), ), ); }