Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 68 additions & 7 deletions lib/controller/classtable_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,19 +92,79 @@ class ClassTableController {
);

late final classTableComputedSignal = computed<ClassTableData>(() {
// 第一步:获取教务课表与体育课程。computed 内同时读取两个信号,
// 体育课程刷新后课表教室会自动重算。
final networkClassTable = schoolClassTableComputedSignal.value;
final sportClasses = SportController.i.sportClassesComputedSignal.value;

// 第二步-a:按(学期、教师、星期、起止节次)汇总体育上课地点,
// 同一语义键对应多个地点时保留为集合,唯一匹配才回填。
final sportLocations = <(String, String, int, int, int), Set<String>>{};
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,
), () => <String>{})
.add(place);
}

// 第二步-b:拷贝一份教务时间安排,只改 classroom,不污染缓存原对象。

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

试试:List.from(networkClassTable.timeArrangement) ?
TimeArrangement 里面的元素应该是不能更改的,所以深拷贝一个有必要。但是 List 应该也是可以改变的,所以可以试试原先数组修改?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试后确认了,List.from 只复制外层列表,直接修改复制列表中的 TimeArrangement.classroom 会同步修改原对象,只有替换成新的 TimeArrangement 元素时原对象才不会变化,我觉得只能逐元素复制(

final timeArrangement = networkClassTable.timeArrangement.map((
arrangement,
) {
return TimeArrangement(
source: arrangement.source,
index: arrangement.index,
weekList: List<bool>.from(arrangement.weekList),
classroom: arrangement.classroom,
teacher: arrangement.teacher,
day: arrangement.day,
start: arrangement.start,
stop: arrangement.stop,
);
}).toList();

for (final arrangement in timeArrangement) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

接着上个,就这里的,timeArrangement 直接改成 networkClassTable.timeArrangement ?
我猜大概率不行,因为不能在 compute 里面改 signal,会触发循环触发链。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个也试了,直接修改在 computed 中可以执行,也没有出现递归触发,但是原始 signal 中的 TimeArrangement 也会被一起修改。运行失败倒是没有,不过会产生 computed 副作用并污染源数据/缓存

// 仅处理教务来源、教室为空且索引合法的安排,已有教室不覆盖。
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<ClassDetail>.from(networkClassTable.classDetail),
notArranged: List<NotArrangementClassDetail>.from(
networkClassTable.notArranged,
),
timeArrangement: List<TimeArrangement>.from(
networkClassTable.timeArrangement,
),
classDetail: networkClassTable.classDetail.map(ClassDetail.from).toList(),
notArranged: networkClassTable.notArranged
.map(NotArrangementClassDetail.from)
.toList(),
timeArrangement: timeArrangement,
classChanges: List<ClassChange>.from(networkClassTable.classChanges),
);
});
Expand Down
4 changes: 4 additions & 0 deletions lib/controller/homepage_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand Down
47 changes: 47 additions & 0 deletions lib/controller/sport_controller.dart
Original file line number Diff line number Diff line change
@@ -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<FetchResult<SportClass>>();
final _lastValidClass = signal<FetchResult<SportClass>?>(null);

final sportClassStateSignal = signal<AsyncState<FetchResult<SportClass>>>(
const AsyncLoading(),
);

late final sportClassesComputedSignal = computed<SportClass>(
() => _lastValidClass.value?.data ?? const <SportClassItem>[],
);

Future<FetchResult<SportClass>> 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;
}
});
}
}
7 changes: 4 additions & 3 deletions lib/page/sport/sport_class_window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,7 +27,7 @@ class _SportClassWindowState extends State<SportClassWindow>
@override
bool get wantKeepAlive => true;

Future<FetchResult<SportClass>> _future = SportSession().getClass();
Future<FetchResult<SportClass>> _future = SportController.i.reloadClass();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

跟此PR无关,但请不用AI对体育中查体测成绩的窗口进行修改,把里面的 SportController() 改成控制器里的实现。


Object? _translateError(BuildContext context, Object? error) {
if (error is SportCredentialMissingException ||
Expand All @@ -45,7 +46,7 @@ class _SportClassWindowState extends State<SportClassWindow>
return RefreshIndicator(
onRefresh: () async {
setState(() {
_future = SportSession().getClass();
_future = SportController.i.reloadClass();
});
},
child: FutureBuilder(
Expand Down Expand Up @@ -109,7 +110,7 @@ class _SportClassWindowState extends State<SportClassWindow>
snapshot.hasError) {
return ReloadWidget(
function: () => setState(() {
_future = SportSession().getClass();
_future = SportController.i.reloadClass();
}),
errorStatus: _translateError(context, snapshot.error),
stackTrace: snapshot.stackTrace,
Expand Down