feat: add undergraduate import and exam selection screens
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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<UndergraduateAuthPage> createState() => _UndergraduateAuthPageState();
|
||||
}
|
||||
|
||||
class _UndergraduateAuthPageState extends State<UndergraduateAuthPage> {
|
||||
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<void> _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<bool>(
|
||||
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)) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (route?.isCurrent == true) Navigator.pop(context);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(exams == null ? '课表已导入,请核对学期开始日。' : '考试信息已更新'),
|
||||
),
|
||||
);
|
||||
} 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<void> _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,
|
||||
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('导入当前课表'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../domain/school_identity.dart';
|
||||
import '../../../domain/school_term.dart';
|
||||
import '../../../domain/undergraduate_exam_query.dart';
|
||||
|
||||
Future<UndergraduateExamQuery?> showUndergraduateExamQueryPicker(
|
||||
BuildContext context, {
|
||||
UndergraduateExamQuery? initial,
|
||||
required DateTime now,
|
||||
}) => showDialog<UndergraduateExamQuery>(
|
||||
context: context,
|
||||
builder: (_) => _ExamPicker(initial: initial, now: now),
|
||||
);
|
||||
|
||||
class _ExamPicker extends StatefulWidget {
|
||||
const _ExamPicker({this.initial, required this.now});
|
||||
final UndergraduateExamQuery? initial;
|
||||
final DateTime now;
|
||||
@override
|
||||
State<_ExamPicker> createState() => _ExamPickerState();
|
||||
}
|
||||
|
||||
class _ExamPickerState extends State<_ExamPicker> {
|
||||
late SchoolTerm _term =
|
||||
widget.initial?.term ??
|
||||
SchoolTerm(523, school: SupportedSchool.uestcUndergraduate);
|
||||
late final Set<UndergraduateExamType> _types = {
|
||||
...?widget.initial?.types,
|
||||
if (widget.initial == null) UndergraduateExamType.finalExam,
|
||||
};
|
||||
@override
|
||||
Widget build(BuildContext context) => AlertDialog(
|
||||
title: const Text('本科考试查询'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('选择一个学期和需要查询的考试类型'),
|
||||
const SizedBox(height: 14),
|
||||
DropdownButton<SchoolTerm>(
|
||||
isExpanded: true,
|
||||
value: _term,
|
||||
items: SchoolTerm.undergraduateChoices(widget.now, saved: _term)
|
||||
.map((t) => DropdownMenuItem(value: t, child: Text(t.label)))
|
||||
.toList(),
|
||||
onChanged: (t) {
|
||||
if (t != null) setState(() => _term = t);
|
||||
},
|
||||
),
|
||||
for (final type in UndergraduateExamType.values)
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(type.label),
|
||||
value: _types.contains(type),
|
||||
onChanged: (v) => setState(() {
|
||||
if (v == true) {
|
||||
_types.add(type);
|
||||
} else {
|
||||
_types.remove(type);
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _types.isEmpty
|
||||
? null
|
||||
: () => Navigator.pop(
|
||||
context,
|
||||
UndergraduateExamQuery(term: _term, types: _types),
|
||||
),
|
||||
child: const Text('开始查询'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user