diff --git a/lib/controller/classtable_controller.dart b/lib/controller/classtable_controller.dart index 48301f34..cc97b1b1 100644 --- a/lib/controller/classtable_controller.dart +++ b/lib/controller/classtable_controller.dart @@ -8,6 +8,7 @@ import 'package:signals/signals.dart'; import 'package:watermeter/controller/custom_class_controller.dart'; import 'package:watermeter/controller/global_timer_controller.dart'; import 'package:watermeter/controller/semester_controller.dart'; +import 'package:watermeter/controller/sport_controller.dart'; import 'package:watermeter/controller/week_swift_controller.dart'; import 'package:watermeter/model/fetch_result.dart'; import 'package:watermeter/model/home_arrangement.dart'; @@ -91,19 +92,79 @@ class ClassTableController { ); late final classTableComputedSignal = computed(() { + // 第一步:获取教务课表与体育课程。computed 内同时读取两个信号, + // 体育课程刷新后课表教室会自动重算。 final networkClassTable = schoolClassTableComputedSignal.value; + final sportClasses = SportController.i.sportClassesComputedSignal.value; + + // 第二步-a:按(学期、教师、星期、起止节次)汇总体育上课地点, + // 同一语义键对应多个地点时保留为集合,唯一匹配才回填。 + final sportLocations = <(String, String, int, int, int), Set>{}; + for (final sportClass in sportClasses) { + final place = sportClass.place.trim(); + if (sportClass.term != networkClassTable.semesterCode || + sportClass.teacher.isEmpty || + place.isEmpty) { + continue; + } + sportLocations + .putIfAbsent(( + sportClass.term, + sportClass.teacher, + sportClass.week, + sportClass.start, + sportClass.stop, + ), () => {}) + .add(place); + } + + // 第二步-b:拷贝一份教务时间安排,只改 classroom,不污染缓存原对象。 + final timeArrangement = networkClassTable.timeArrangement.map(( + arrangement, + ) { + return TimeArrangement( + source: arrangement.source, + index: arrangement.index, + weekList: List.from(arrangement.weekList), + classroom: arrangement.classroom, + teacher: arrangement.teacher, + day: arrangement.day, + start: arrangement.start, + stop: arrangement.stop, + ); + }).toList(); + + for (final arrangement in timeArrangement) { + // 仅处理教务来源、教室为空且索引合法的安排,已有教室不覆盖。 + if (arrangement.source != Source.school || + arrangement.classroom?.trim().isNotEmpty == true || + arrangement.index < 0 || + arrangement.index >= networkClassTable.classDetail.length) { + continue; + } + final matchedLocations = + sportLocations[( + networkClassTable.semesterCode, + arrangement.teacher ?? '', + arrangement.day, + arrangement.start, + arrangement.stop, + )]; + if (matchedLocations?.length == 1) { + arrangement.classroom = matchedLocations!.single; + } + } + // 第二步-c:输出全新的 ClassTableData。 return ClassTableData( semesterLength: networkClassTable.semesterLength, semesterCode: networkClassTable.semesterCode, termStartDay: networkClassTable.termStartDay, - classDetail: List.from(networkClassTable.classDetail), - notArranged: List.from( - networkClassTable.notArranged, - ), - timeArrangement: List.from( - networkClassTable.timeArrangement, - ), + classDetail: networkClassTable.classDetail.map(ClassDetail.from).toList(), + notArranged: networkClassTable.notArranged + .map(NotArrangementClassDetail.from) + .toList(), + timeArrangement: timeArrangement, classChanges: List.from(networkClassTable.classChanges), ); }); diff --git a/lib/controller/homepage_controller.dart b/lib/controller/homepage_controller.dart index 5368efd7..132e4dc2 100644 --- a/lib/controller/homepage_controller.dart +++ b/lib/controller/homepage_controller.dart @@ -13,6 +13,7 @@ import 'package:watermeter/controller/other_experiment_controller.dart'; import 'package:watermeter/controller/physics_experiment_controller.dart'; import 'package:watermeter/controller/school_card_controller.dart'; import 'package:watermeter/controller/semester_controller.dart'; +import 'package:watermeter/controller/sport_controller.dart'; import 'package:watermeter/controller/week_swift_controller.dart'; import 'package:watermeter/model/home_arrangement.dart'; import 'package:watermeter/model/password_exceptions.dart'; @@ -118,6 +119,9 @@ class HomepageController { "OtherExperiment", OtherExperimentController.i.reloadOtherExperiment, ), + _safeReload("Sport", () async { + await SportController.i.reloadClass(); + }), _safeReload("Library", LibraryController.i.reloadBorrowList), _safeReload("SchoolCard", SchoolCardController.i.reloadOverview), _safeReload("Electricity", EnergyController.i.refreshElectricityInfo), diff --git a/lib/controller/sport_controller.dart b/lib/controller/sport_controller.dart new file mode 100644 index 00000000..aca56706 --- /dev/null +++ b/lib/controller/sport_controller.dart @@ -0,0 +1,47 @@ +// Copyright 2025 Traintime PDA authors. +// SPDX-License-Identifier: MPL-2.0 + +import 'package:signals/signals.dart'; +import 'package:watermeter/model/fetch_result.dart'; +import 'package:watermeter/model/xidian_sport/sport_class.dart'; +import 'package:watermeter/repository/logger.dart'; +import 'package:watermeter/repository/miscellaneous_session/xidian_sport_session.dart'; +import 'package:watermeter/repository/single_flight.dart'; + +class SportController { + static final SportController i = SportController._(); + + SportController._(); + + final SportSession session = SportSession(); + final _classFlight = SingleFlight>(); + final _lastValidClass = signal?>(null); + + final sportClassStateSignal = signal>>( + const AsyncLoading(), + ); + + late final sportClassesComputedSignal = computed( + () => _lastValidClass.value?.data ?? const [], + ); + + Future> reloadClass() { + final previous = _lastValidClass.value; + sportClassStateSignal.value = previous != null + ? AsyncState.dataRefreshing(previous) + : AsyncState.loading(); + + return _classFlight.run(() async { + try { + final result = await session.getClass(); + _lastValidClass.value = result; + sportClassStateSignal.set(AsyncState.data(result), force: true); + return result; + } catch (e, s) { + sportClassStateSignal.value = AsyncState.error(e, s); + log.handle(e, s, '[SportController][reloadClass] Have issue'); + rethrow; + } + }); + } +} diff --git a/lib/page/sport/sport_class_window.dart b/lib/page/sport/sport_class_window.dart index a0f30a77..5c0334e3 100644 --- a/lib/page/sport/sport_class_window.dart +++ b/lib/page/sport/sport_class_window.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:watermeter/controller/sport_controller.dart'; import 'package:watermeter/model/fetch_result.dart'; import 'package:watermeter/model/xidian_sport/sport_class.dart'; import 'package:watermeter/page/public_widget/cache_alerter.dart'; @@ -26,7 +27,7 @@ class _SportClassWindowState extends State @override bool get wantKeepAlive => true; - Future> _future = SportSession().getClass(); + Future> _future = SportController.i.reloadClass(); Object? _translateError(BuildContext context, Object? error) { if (error is SportCredentialMissingException || @@ -45,7 +46,7 @@ class _SportClassWindowState extends State return RefreshIndicator( onRefresh: () async { setState(() { - _future = SportSession().getClass(); + _future = SportController.i.reloadClass(); }); }, child: FutureBuilder( @@ -109,7 +110,7 @@ class _SportClassWindowState extends State snapshot.hasError) { return ReloadWidget( function: () => setState(() { - _future = SportSession().getClass(); + _future = SportController.i.reloadClass(); }), errorStatus: _translateError(context, snapshot.error), stackTrace: snapshot.stackTrace,