import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../../app/app_controller.dart'; import '../../../data/json_schedule_import.dart'; import '../../../domain/exam_schedule.dart'; import '../../profile/import_preview.dart'; import 'session.dart'; import 'exam_query_picker.dart'; class UndergraduateAuthPage extends StatefulWidget { const UndergraduateAuthPage({ super.key, required this.session, required this.controller, }); final UndergraduateSession session; final AppController controller; @override State createState() => _UndergraduateAuthPageState(); } class _UndergraduateAuthPageState extends State { bool _saving = false; Object? _handled; String? _saveError; @override void initState() { super.initState(); widget.session.addListener(_changed); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { widget.session.setReadingActive(true); _changed(); } }); } void _changed() { if (!mounted) return; setState(() {}); final s = widget.session, result = s.pendingDraft ?? s.pendingExams; if (_saving || result == null || identical(_handled, result)) return; _handled = result; _saving = true; final generation = s.pageGeneration; WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) return; await _save(s.pendingDraft, s.pendingExams, generation); }); } Future _save( ImportDraft? draft, ExamSchedule? exams, int generation, ) async { final s = widget.session, controller = widget.controller, route = ModalRoute.of(context); bool current() => mounted && route?.isActive == true && generation == s.pageGeneration && identical(draft, s.pendingDraft) && identical(exams, s.pendingExams); try { if (!current()) return; var saved = false; if (draft != null) { final replacement = controller.snapshot.courses.isNotEmpty && controller.snapshot.schoolTerm != draft.schoolTerm; if (replacement || draft.courses.isEmpty) { final confirmed = await showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (_) => ImportPreview( draft: draft, controller: controller, canApply: current, ), ); if (!current()) return; saved = confirmed == true; } else { await controller.applyImport( draft, confirmedEmpty: false, canApply: current, ); saved = controller.lastError == null; } } else if (exams != null) { await controller.applyExams(exams, canApply: current); saved = controller.lastError == null; } if (!current()) return; if (saved && s.saved(generation: generation)) { if (!mounted) return; final messenger = ScaffoldMessenger.of(context); if (route?.isCurrent == true) Navigator.pop(context); messenger.showSnackBar( SnackBar( content: Text( exams != null ? '考试信息已更新' : draft!.courses.any((c) => c.room == '教室待定') ? '课表已导入,部分课程教室待定;请核对学期开始日。' : '课表已导入,请核对学期开始日。', ), ), ); } else if (controller.lastError != null) { _saveError = controller.lastError; } } finally { _saving = false; if (mounted) _changed(); } } @override void dispose() { widget.session.removeListener(_changed); widget.session.setReadingActive(false); super.dispose(); } Future _changeQuery() async { final value = await showUndergraduateExamQueryPicker( context, initial: widget.session.query, now: widget.controller.clock(), ); if (!mounted || value == null) return; _handled = null; _saveError = null; await widget.session.openExams(value); } @override Widget build(BuildContext context) { final s = widget.session; return Scaffold( appBar: AppBar( title: Text(s.requestingExams ? '本科考试查询' : '本科课表导入'), actions: [ if (s.requestingExams) IconButton( tooltip: '查询选项', onPressed: _saving ? null : _changeQuery, icon: const Icon(Icons.tune_rounded), ), IconButton( tooltip: '重新认证', onPressed: _saving ? null : () { _handled = null; _saveError = null; s.reauthenticate(); }, icon: const Icon(Icons.refresh_rounded), ), ], ), body: Column( children: [ if (s.busy || _saving) const LinearProgressIndicator(minHeight: 2), Container( width: double.infinity, padding: const EdgeInsets.all(12), color: const Color(0xFFEFF6FF), child: Text( [ s.message, if (s.browserVersion != null) 'Chromium ${s.browserVersion}', ].join('\n'), style: const TextStyle(fontSize: 12, height: 1.5), ), ), if (s.requestingExams && s.query != null) Padding( padding: const EdgeInsets.all(8), child: Text( '${s.query!.term.label} · ${s.query!.orderedTypes.map((t) => t.label).join('、')}', style: const TextStyle(fontSize: 12), ), ), if (_saveError != null) Padding( padding: const EdgeInsets.all(8), child: Column( children: [ Text(_saveError!, style: const TextStyle(fontSize: 12)), TextButton( onPressed: _saving ? null : () { _saveError = null; _handled = null; _changed(); }, child: const Text('重试保存'), ), ], ), ), Expanded(child: WebViewWidget(controller: s.web)), if (!s.requestingExams) SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 10), child: SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: _saving || s.busy ? null : () { _handled = null; _saveError = null; s.readCurrentTimetable(); }, icon: const Icon(Icons.download_rounded), label: const Text('导入当前课表'), ), ), ), ), ], ), ); } }