feat: select school terms and smooth refractive Dock motion
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../data/json_schedule_import.dart';
|
||||
import '../../domain/exam_schedule.dart';
|
||||
import '../../domain/school_term.dart';
|
||||
import 'school_navigation_policy.dart';
|
||||
import 'school_page_adapter.dart';
|
||||
import 'school_exam_adapter.dart';
|
||||
@@ -114,6 +115,23 @@ class SchoolSession extends ChangeNotifier {
|
||||
int _openSerial = 0;
|
||||
ImportDraft? pendingDraft;
|
||||
ExamSchedule? pendingExams;
|
||||
SchoolTerm? selectedTerm;
|
||||
bool _termSelectionRequested = true;
|
||||
bool _termSelectionReady = false;
|
||||
List<SchoolTerm> _liveTerms = const [];
|
||||
bool get needsTermSelection =>
|
||||
hasOpened &&
|
||||
connectionState == SchoolConnectionState.connected &&
|
||||
_termSelectionRequested &&
|
||||
_termSelectionReady;
|
||||
List<SchoolTerm> get termChoices {
|
||||
final terms = [...(_liveTerms.isEmpty ? SchoolTerm.choices : _liveTerms)];
|
||||
if (selectedTerm != null && !terms.contains(selectedTerm)) {
|
||||
terms.add(selectedTerm!);
|
||||
}
|
||||
return terms..sort((a, b) => b.id.compareTo(a.id));
|
||||
}
|
||||
|
||||
String message = '请在学校网页中完成登录与验证码。';
|
||||
String get statusLabel => logoutFailed
|
||||
? '学校退出未完成,请重试'
|
||||
@@ -130,18 +148,31 @@ class SchoolSession extends ChangeNotifier {
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/jzsso/login',
|
||||
);
|
||||
static final timetableUrl = Uri.parse(
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1',
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73',
|
||||
);
|
||||
static final examUrl = Uri.parse(
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/kskcxx/index',
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/kskcxx/index?xq=73',
|
||||
);
|
||||
Uri get _target => requestingExams ? examUrl : timetableUrl;
|
||||
Uri get _target => requestingExams
|
||||
? (selectedTerm?.examUri ?? examUrl)
|
||||
: (selectedTerm?.timetableUri ?? timetableUrl);
|
||||
Future<void> open() => _open(exams: false);
|
||||
Future<void> openTimetable() => _open(exams: false);
|
||||
Future<void> openExams() => _open(exams: true);
|
||||
Future<void> openAuthentication() =>
|
||||
_open(exams: requestingExams, forceAuth: true);
|
||||
Future<void> _open({required bool exams, bool forceAuth = false}) async {
|
||||
Future<void> openTimetable({SchoolTerm? term, bool chooseTerm = false}) =>
|
||||
_open(exams: false, term: term, chooseTerm: chooseTerm);
|
||||
Future<void> openExams({SchoolTerm? term, bool chooseTerm = false}) =>
|
||||
_open(exams: true, term: term, chooseTerm: chooseTerm);
|
||||
Future<void> openAuthentication() => _open(
|
||||
exams: requestingExams,
|
||||
term: selectedTerm,
|
||||
chooseTerm: _termSelectionRequested,
|
||||
forceAuth: true,
|
||||
);
|
||||
Future<void> _open({
|
||||
required bool exams,
|
||||
SchoolTerm? term,
|
||||
bool chooseTerm = false,
|
||||
bool forceAuth = false,
|
||||
}) async {
|
||||
final serial = ++_openSerial;
|
||||
final closing = _logoutFuture;
|
||||
if (closing != null) await closing;
|
||||
@@ -154,6 +185,9 @@ class SchoolSession extends ChangeNotifier {
|
||||
final generation = _pageGeneration;
|
||||
pendingDraft = null;
|
||||
pendingExams = null;
|
||||
selectedTerm = term;
|
||||
_termSelectionRequested = chooseTerm || term == null;
|
||||
_termSelectionReady = false;
|
||||
requestingExams = exams;
|
||||
logoutFailed = false;
|
||||
hasOpened = true;
|
||||
@@ -161,10 +195,17 @@ class SchoolSession extends ChangeNotifier {
|
||||
_flow.begin(_target);
|
||||
final connected =
|
||||
connectionState == SchoolConnectionState.connected && !forceAuth;
|
||||
finding = connected;
|
||||
message = connected ? _findingMessage : '请在学校网页中完成登录与验证码。';
|
||||
if (connected) _armTimeout();
|
||||
_termSelectionReady = connected;
|
||||
finding = connected && !needsTermSelection;
|
||||
_loading = !needsTermSelection;
|
||||
message = needsTermSelection
|
||||
? '请选择要导入的学校学期。'
|
||||
: connected
|
||||
? _findingMessage
|
||||
: '请在学校网页中完成登录与验证码。';
|
||||
if (finding) _armTimeout();
|
||||
notifyListeners();
|
||||
if (needsTermSelection) return;
|
||||
try {
|
||||
await web.loadRequest(connected ? _target : authUrl);
|
||||
} catch (_) {
|
||||
@@ -174,6 +215,39 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectTerm(SchoolTerm term) async {
|
||||
if (!hasOpened ||
|
||||
!needsTermSelection ||
|
||||
connectionState != SchoolConnectionState.connected ||
|
||||
!termChoices.contains(term)) {
|
||||
return;
|
||||
}
|
||||
final serial = ++_openSerial;
|
||||
_poll?.cancel();
|
||||
_timeout?.cancel();
|
||||
_pageGeneration++;
|
||||
_reader.invalidate();
|
||||
final generation = _pageGeneration;
|
||||
pendingDraft = null;
|
||||
pendingExams = null;
|
||||
selectedTerm = term;
|
||||
_termSelectionRequested = false;
|
||||
_manualRead = false;
|
||||
_flow.begin(_target);
|
||||
finding = true;
|
||||
_loading = true;
|
||||
message = _findingMessage;
|
||||
_armTimeout();
|
||||
notifyListeners();
|
||||
try {
|
||||
await web.loadRequest(_target);
|
||||
} catch (_) {
|
||||
if (serial == _openSerial && generation == _pageGeneration) {
|
||||
_failure('学校学期查询页面暂时无法打开,请重试。');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String get _findingMessage => requestingExams ? '正在查找考试安排,请稍候' : '正在查找课表,请稍候';
|
||||
void _armTimeout() {
|
||||
_timeout?.cancel();
|
||||
@@ -223,6 +297,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
if (!_readingActive ||
|
||||
!hasOpened ||
|
||||
generation != _pageGeneration ||
|
||||
needsTermSelection ||
|
||||
pendingDraft != null ||
|
||||
pendingExams != null) {
|
||||
return;
|
||||
@@ -258,6 +333,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
_loading = false;
|
||||
if (pendingDraft != null || pendingExams != null) return;
|
||||
if (uri.host == 'idas.uestc.edu.cn') {
|
||||
_termSelectionReady = false;
|
||||
_timeout?.cancel();
|
||||
finding = false;
|
||||
connectionState = SchoolConnectionState.authenticating;
|
||||
@@ -275,7 +351,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
final targetPage = requestingExams
|
||||
? SchoolNavigationPolicy.isExam(uri)
|
||||
: SchoolNavigationPolicy.isTimetable(uri);
|
||||
if (targetPage || _manualRead) {
|
||||
if (!_termSelectionRequested && (targetPage || _manualRead)) {
|
||||
finding = true;
|
||||
message = _findingMessage;
|
||||
notifyListeners();
|
||||
@@ -288,21 +364,33 @@ class SchoolSession extends ChangeNotifier {
|
||||
await web.currentUrl() != url) {
|
||||
return;
|
||||
}
|
||||
if (evidence.terms.isNotEmpty) _liveTerms = evidence.terms;
|
||||
if (_termSelectionRequested) {
|
||||
if (evidence.authenticated || evidence.pages.isNotEmpty) {
|
||||
connectionState = SchoolConnectionState.connected;
|
||||
_termSelectionReady = true;
|
||||
_timeout?.cancel();
|
||||
_poll?.cancel();
|
||||
finding = false;
|
||||
message = '请选择要导入的学校学期。';
|
||||
notifyListeners();
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (final page in evidence.pages) {
|
||||
if (page.kind != (requestingExams ? 'exams' : 'timetable')) continue;
|
||||
try {
|
||||
if (requestingExams) {
|
||||
final draft = SchoolExamAdapter.extract(page.html);
|
||||
if (generation != _pageGeneration) return;
|
||||
if (draft.termId != selectedTerm?.id.toString()) continue;
|
||||
pendingExams = draft;
|
||||
} else {
|
||||
// The probe validates the live document's origin. The strict parser
|
||||
// gets only its recognized fragment with the canonical table path.
|
||||
final draft = await _reader.read(
|
||||
timetableUrl,
|
||||
() async => page.html,
|
||||
);
|
||||
final draft = await _reader.read(_target, () async => page.html);
|
||||
if (!hasOpened || generation != _pageGeneration) return;
|
||||
if (draft.schoolTerm != selectedTerm) continue;
|
||||
pendingDraft = draft;
|
||||
}
|
||||
_timeout?.cancel();
|
||||
@@ -347,6 +435,9 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<ImportDraft> readTimetable() async {
|
||||
if (_termSelectionRequested || selectedTerm == null) {
|
||||
throw UnsupportedSchoolLayout();
|
||||
}
|
||||
final generation = _pageGeneration;
|
||||
final uri = Uri.tryParse(await web.currentUrl() ?? '');
|
||||
if (uri == null || !SchoolNavigationPolicy.isGraduatePage(uri)) {
|
||||
@@ -360,7 +451,12 @@ class SchoolSession extends ChangeNotifier {
|
||||
for (final page in evidence.pages) {
|
||||
if (page.kind != 'timetable') continue;
|
||||
try {
|
||||
return await _reader.read(timetableUrl, () async => page.html);
|
||||
final draft = await _reader.read(
|
||||
selectedTerm!.timetableUri,
|
||||
() async => page.html,
|
||||
);
|
||||
if (draft.schoolTerm != selectedTerm) continue;
|
||||
return draft;
|
||||
} on UnsupportedSchoolLayout {
|
||||
if (generation != _pageGeneration) rethrow;
|
||||
}
|
||||
@@ -369,6 +465,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> readCurrentPage() async {
|
||||
if (needsTermSelection) return;
|
||||
_poll?.cancel();
|
||||
_timeout?.cancel();
|
||||
_pageGeneration++;
|
||||
|
||||
Reference in New Issue
Block a user