diff --git a/assets/flutter_i18n/en_US.yaml b/assets/flutter_i18n/en_US.yaml index f9d952d3..08b32cc7 100644 --- a/assets/flutter_i18n/en_US.yaml +++ b/assets/flutter_i18n/en_US.yaml @@ -154,6 +154,8 @@ classtable: output_to_system: "Export to system calendar" refresh_classtable: "Refresh schedule" switch_semester: "Switch classtable semester" + enable_current_time_indicator: "Show current time indicator" + disable_current_time_indicator: "Hide current time indicator" class_change_page: title: "Schedule Changes" empty_message: "Currently there's no class schedule changes" diff --git a/assets/flutter_i18n/zh_CN.yaml b/assets/flutter_i18n/zh_CN.yaml index af2c9aae..99e38aff 100644 --- a/assets/flutter_i18n/zh_CN.yaml +++ b/assets/flutter_i18n/zh_CN.yaml @@ -153,6 +153,8 @@ classtable: output_to_system: "导出到系统日历" refresh_classtable: "刷新日程表" switch_semester: "切换课程表学期" + enable_current_time_indicator: "显示当前时间指示条" + disable_current_time_indicator: "隐藏当前时间指示条" class_change_page: title: "课程调整" empty_message: "目前没有调课信息" diff --git a/assets/flutter_i18n/zh_TW.yaml b/assets/flutter_i18n/zh_TW.yaml index 54e3c506..45d8e2a6 100644 --- a/assets/flutter_i18n/zh_TW.yaml +++ b/assets/flutter_i18n/zh_TW.yaml @@ -127,6 +127,8 @@ classtable: output_to_system: 導出到系統日曆 refresh_classtable: 刷新日程表 switch_semester: 切換課程表學期 + enable_current_time_indicator: 顯示當前時間指示條 + disable_current_time_indicator: 隱藏當前時間指示條 class_change_page: title: 課程調整 empty_message: 目前沒有調課信息 diff --git a/lib/page/classtable/class_page/content_classtable_page.dart b/lib/page/classtable/class_page/content_classtable_page.dart index b05021a1..3ebf5fd1 100644 --- a/lib/page/classtable/class_page/content_classtable_page.dart +++ b/lib/page/classtable/class_page/content_classtable_page.dart @@ -239,6 +239,19 @@ class _ContentClassTablePageState extends State { ), ), ), + PopupMenuItem( + value: 'J', + child: Text( + FlutterI18n.translate( + context, + preference.getBool( + preference.Preference.enableCurrentTimeIndicator, + ) + ? "classtable.popup_menu.disable_current_time_indicator" + : "classtable.popup_menu.enable_current_time_indicator", + ), + ), + ), ], onSelected: (String action) async { final box = context.findRenderObject() as RenderBox?; @@ -485,6 +498,18 @@ class _ContentClassTablePageState extends State { } }); } + break; + case 'J': + bool currentValue = preference.getBool( + preference.Preference.enableCurrentTimeIndicator, + ); + await preference.setBool( + preference.Preference.enableCurrentTimeIndicator, + !currentValue, + ); + if (context.mounted) { + setState(() {}); + } } }, ), diff --git a/lib/page/classtable/class_table_view/class_organized_data.dart b/lib/page/classtable/class_table_view/class_organized_data.dart index a7fc8054..742a001e 100644 --- a/lib/page/classtable/class_table_view/class_organized_data.dart +++ b/lib/page/classtable/class_table_view/class_organized_data.dart @@ -6,6 +6,7 @@ // Removed left/right, only use stack. import 'package:flutter/material.dart'; +import 'package:watermeter/model/time_list.dart'; import 'package:watermeter/model/xidian_ids/exam.dart'; import 'package:watermeter/model/xidian_ids/classtable.dart'; import 'package:watermeter/model/xidian_ids/experiment.dart'; @@ -121,7 +122,7 @@ class ClassOrgainzedData { this.place, }); - static double _transferIndex(DateTime time) { + static double transferIndex(DateTime time) { int timeInMin = time.hour * 60 + time.minute; int previous = 0; // Start from the second element. @@ -162,6 +163,46 @@ class ClassOrgainzedData { return 61; } + static double transferIndexForIndicator(DateTime time) { + int minutesOfDay(String value) { + final timeParts = value.split(":"); + return int.parse(timeParts[0]) * 60 + int.parse(timeParts[1]); + } + + double indexOfMinute(int value) { + return transferIndex( + DateTime(time.year, time.month, time.day, value ~/ 60, value % 60), + ); + } + + final timeInMin = time.hour * 60 + time.minute; + + for (int i = 0; i < timeList.length; i += 2) { + final start = minutesOfDay(timeList[i]); + final stop = minutesOfDay(timeList[i + 1]); + final nextStart = i + 2 < timeList.length + ? minutesOfDay(timeList[i + 2]) + : null; + final isSmallBreak = nextStart != null && nextStart - stop <= 20; + final visualStop = isSmallBreak ? nextStart : stop; + + // During class, map the elapsed class time across the visual block including the following small break. + if (timeInMin >= start && timeInMin < stop) { + final startIndex = indexOfMinute(start); + final stopIndex = indexOfMinute(visualStop); + return startIndex + + (stopIndex - startIndex) * (timeInMin - start) / (stop - start); + } + + // During a small break, show the indicator at the end of the class. + if (isSmallBreak && timeInMin >= stop && timeInMin < visualStop) { + return indexOfMinute(visualStop); + } + } + + return transferIndex(time); + } + ClassOrgainzedData._({ required this.data, required DateTime start, @@ -170,7 +211,7 @@ class ClassOrgainzedData { required this.name, this.place, }) { - this.start = _transferIndex(start); - this.stop = _transferIndex(stop); + this.start = transferIndex(start); + this.stop = transferIndex(stop); } } diff --git a/lib/page/classtable/class_table_view/class_table_view.dart b/lib/page/classtable/class_table_view/class_table_view.dart index c2d0c252..5c217444 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -164,6 +164,13 @@ class _ClassTableViewState extends State { } } + /// This function will be triggered every minute to refresh the time indicator. + void _reloadTimeIndicator() { + if (mounted && classTableState.currentWeek == widget.index) { + setState(() {}); + } + } + void updateSize() => size = ClassTableState.of(context)!.constraints; @override @@ -171,12 +178,14 @@ class _ClassTableViewState extends State { super.didChangeDependencies(); classTableState = ClassTableState.of(context)!.controllers; classTableState.addListener(_reload); + classTableState.currentTimeNotifier.addListener(_reloadTimeIndicator); updateSize(); } @override void dispose() { classTableState.removeListener(_reload); + classTableState.currentTimeNotifier.removeListener(_reloadTimeIndicator); super.dispose(); } @@ -186,6 +195,31 @@ class _ClassTableViewState extends State { updateSize(); } + Widget _buildCurrentTimeIndicator() { + if (classTableState.currentWeek != widget.index) { + return const SizedBox.shrink(); + } + if (!preference.getBool(preference.Preference.enableCurrentTimeIndicator)) { + return const SizedBox.shrink(); + } + // Keep the indicator correct during short breaks between classes. + final timeIndex = ClassOrgainzedData.transferIndexForIndicator( + classTableState.currentTimeNotifier.value, + ); + if (timeIndex <= 0 || timeIndex >= 61) { + return const SizedBox.shrink(); + } + return Positioned( + top: blockheight(timeIndex) - 1, + left: leftRow, + width: size.maxWidth - leftRow, + height: 2, + child: Container( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), + ), + ); + } + @override Widget build(BuildContext context) { return [ @@ -208,6 +242,7 @@ class _ClassTableViewState extends State { .constrained(width: leftRow) .positioned(left: 0), ...classSubRow(true), + _buildCurrentTimeIndicator(), ] .toStack() .constrained(height: blockheight(61), width: size.maxWidth) diff --git a/lib/page/classtable/classtable_state.dart b/lib/page/classtable/classtable_state.dart index 4ebc870b..de225917 100644 --- a/lib/page/classtable/classtable_state.dart +++ b/lib/page/classtable/classtable_state.dart @@ -2,6 +2,7 @@ // Copyright 2025 Traintime PDA authors. // SPDX-License-Identifier: MPL-2.0 OR Apache-2.0 +import 'dart:async'; import 'dart:math' as math; import 'package:device_calendar/device_calendar.dart'; @@ -54,8 +55,15 @@ class ClassTableWidgetState with ChangeNotifier { ///*******************************************************************/// bool _disposed = false; + /// A notifier that fires every minute with the current time. + final ValueNotifier currentTimeNotifier = + ValueNotifier(DateTime.now()); + Timer? _currentTimeTimer; + @override void dispose() { + _currentTimeTimer?.cancel(); + currentTimeNotifier.dispose(); _disposed = true; super.dispose(); } @@ -461,6 +469,9 @@ END:VTIMEZONE } else { _chosenWeek = currentWeek; } + _currentTimeTimer = Timer.periodic(const Duration(minutes: 1), (_) { + currentTimeNotifier.value = DateTime.now(); + }); } bool _checkIsOverlapping( diff --git a/lib/repository/preference.dart b/lib/repository/preference.dart index 59cdf4dd..ecf55147 100644 --- a/lib/repository/preference.dart +++ b/lib/repository/preference.dart @@ -89,7 +89,8 @@ enum Preference { ), // 上次通知使用的语言 dormWaterToken(key: "dorm_water_token", type: "String"), // 宿舍水机登录 token dormWaterUid(key: "dorm_water_uid", type: "String"), // 宿舍水机用户 uid - dormWaterEid(key: "dorm_water_eid", type: "String"); // 宿舍水机用户 eid + dormWaterEid(key: "dorm_water_eid", type: "String"), // 宿舍水机用户 eid + enableCurrentTimeIndicator(key: "enableCurrentTimeIndicator", type: "bool"); // 是否启用当前时间指示条 const Preference({required this.key, this.type = "String"});