From f6b627bdcd0981d5b77830cd03f9ff99724fc4c7 Mon Sep 17 00:00:00 2001 From: nkanf Date: Wed, 15 Apr 2026 14:44:28 +0800 Subject: [PATCH 1/4] feat: add current time indicator. --- assets/flutter_i18n/en_US.yaml | 2 ++ assets/flutter_i18n/zh_CN.yaml | 2 ++ assets/flutter_i18n/zh_TW.yaml | 2 ++ .../class_page/content_classtable_page.dart | 24 +++++++++++++++ .../class_organized_data.dart | 6 ++-- .../class_table_view/class_table_view.dart | 30 +++++++++++++++++++ lib/repository/preference.dart | 3 +- 7 files changed, 65 insertions(+), 4 deletions(-) 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..ede2abc0 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,17 @@ class _ContentClassTablePageState extends State { } }); } + 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..f7126d59 100644 --- a/lib/page/classtable/class_table_view/class_organized_data.dart +++ b/lib/page/classtable/class_table_view/class_organized_data.dart @@ -121,7 +121,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. @@ -170,7 +170,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..a33138e5 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:signals/signals_flutter.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:watermeter/controller/global_timer_controller.dart'; import 'package:watermeter/model/time_list.dart'; import 'package:watermeter/page/classtable/class_table_view/class_card.dart'; @@ -208,6 +210,34 @@ class _ClassTableViewState extends State { .constrained(width: leftRow) .positioned(left: 0), ...classSubRow(true), + Watch((context) { + final now = GlobalTimerController.i.currentTimeSignal.value; + if (classTableState.currentWeek != widget.index) { + return const SizedBox.shrink(); + } + + // Check if current time indicator is enabled + if (!preference.getBool(preference.Preference.enableCurrentTimeIndicator)) { + return const SizedBox.shrink(); + } + + // Check if it is within the day's class range (approx 8:30 - 21:25) + // transferIndex returns 0 if before 8:30 and 61 if after 21:25. + final index = ClassOrgainzedData.transferIndex(now); + if (index <= 0 || index >= 61) { + return const SizedBox.shrink(); + } + + return Positioned( + top: blockheight(index) - 1, + left: 0, + width: size.maxWidth, + height: 2, + child: Container( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), + ), + ); + }), ] .toStack() .constrained(height: blockheight(61), width: size.maxWidth) 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"}); From 40291b23c47107f2386a2bbb8ce9dc7aa85d04e4 Mon Sep 17 00:00:00 2001 From: nkanf Date: Thu, 16 Apr 2026 15:33:04 +0800 Subject: [PATCH 2/4] fix: add missing break and justify indicator left position. --- lib/page/classtable/class_page/content_classtable_page.dart | 1 + lib/page/classtable/class_table_view/class_table_view.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/page/classtable/class_page/content_classtable_page.dart b/lib/page/classtable/class_page/content_classtable_page.dart index ede2abc0..3ebf5fd1 100644 --- a/lib/page/classtable/class_page/content_classtable_page.dart +++ b/lib/page/classtable/class_page/content_classtable_page.dart @@ -498,6 +498,7 @@ class _ContentClassTablePageState extends State { } }); } + break; case 'J': bool currentValue = preference.getBool( preference.Preference.enableCurrentTimeIndicator, 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 a33138e5..5c60ad97 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -230,8 +230,8 @@ class _ClassTableViewState extends State { return Positioned( top: blockheight(index) - 1, - left: 0, - width: size.maxWidth, + left: leftRow, + width: size.maxWidth - leftRow, height: 2, child: Container( color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), From d4ea1b49d4e670dbbed8b4db40877f4acd2e9536 Mon Sep 17 00:00:00 2001 From: nkanf Date: Thu, 16 Apr 2026 17:20:30 +0800 Subject: [PATCH 3/4] fix: change to ChangeNotifier way for current time state. --- .../class_table_view/class_table_view.dart | 65 ++++++++++--------- lib/page/classtable/classtable_state.dart | 11 ++++ 2 files changed, 46 insertions(+), 30 deletions(-) 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 5c60ad97..99dd8b43 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -4,9 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:signals/signals_flutter.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:watermeter/controller/global_timer_controller.dart'; import 'package:watermeter/model/time_list.dart'; import 'package:watermeter/page/classtable/class_table_view/class_card.dart'; @@ -166,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 @@ -173,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(); } @@ -188,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(); + } + // transferIndex returns 0 if before 8:30 and 61 if after 21:25. + final timeIndex = ClassOrgainzedData.transferIndex( + 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 [ @@ -210,34 +242,7 @@ class _ClassTableViewState extends State { .constrained(width: leftRow) .positioned(left: 0), ...classSubRow(true), - Watch((context) { - final now = GlobalTimerController.i.currentTimeSignal.value; - if (classTableState.currentWeek != widget.index) { - return const SizedBox.shrink(); - } - - // Check if current time indicator is enabled - if (!preference.getBool(preference.Preference.enableCurrentTimeIndicator)) { - return const SizedBox.shrink(); - } - - // Check if it is within the day's class range (approx 8:30 - 21:25) - // transferIndex returns 0 if before 8:30 and 61 if after 21:25. - final index = ClassOrgainzedData.transferIndex(now); - if (index <= 0 || index >= 61) { - return const SizedBox.shrink(); - } - - return Positioned( - top: blockheight(index) - 1, - left: leftRow, - width: size.maxWidth - leftRow, - height: 2, - child: Container( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), - ), - ); - }), + _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( From 6f4833f2c56b7fe4e10998cb86c8c2faf129a877 Mon Sep 17 00:00:00 2001 From: nkanf Date: Fri, 17 Apr 2026 17:47:15 +0800 Subject: [PATCH 4/4] fix: correct time indicator position during short breaks. Co-authored-by: Copilot --- .../class_organized_data.dart | 41 +++++++++++++++++++ .../class_table_view/class_table_view.dart | 4 +- 2 files changed, 43 insertions(+), 2 deletions(-) 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 f7126d59..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'; @@ -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, 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 99dd8b43..5c217444 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -202,8 +202,8 @@ class _ClassTableViewState extends State { if (!preference.getBool(preference.Preference.enableCurrentTimeIndicator)) { return const SizedBox.shrink(); } - // transferIndex returns 0 if before 8:30 and 61 if after 21:25. - final timeIndex = ClassOrgainzedData.transferIndex( + // Keep the indicator correct during short breaks between classes. + final timeIndex = ClassOrgainzedData.transferIndexForIndicator( classTableState.currentTimeNotifier.value, ); if (timeIndex <= 0 || timeIndex >= 61) {