From c8a2c2b9e7370a64876435f13dc00fb254ef8cd3 Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Thu, 16 Apr 2026 22:19:52 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AF=BE?= =?UTF-8?q?=E8=A1=A8=E8=A7=86=E8=A7=89=E8=AE=BE=E7=BD=AE=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E5=B7=B2?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=AF=BE=E7=A8=8B=E7=9A=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E5=92=8C=E5=BD=93=E5=89=8D=E6=97=B6=E9=97=B4=E6=8C=87=E7=A4=BA?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gradle/wrapper/gradle-wrapper.properties | 4 +- assets/flutter_i18n/en_US.yaml | 11 + assets/flutter_i18n/zh_CN.yaml | 11 + assets/flutter_i18n/zh_TW.yaml | 11 + .../class_page/content_classtable_page.dart | 192 ++++++++++++++++++ .../class_table_view/class_card.dart | 29 ++- .../class_organized_data.dart | 6 + .../class_table_view/class_table_view.dart | 33 ++- .../completed_class_style.dart | 113 +++++++++++ .../current_time_indicator.dart | 137 +++++++++++++ lib/page/classtable/classtable_state.dart | 4 + pubspec.lock | 8 +- 12 files changed, 536 insertions(+), 23 deletions(-) create mode 100644 lib/page/classtable/class_table_view/completed_class_style.dart create mode 100644 lib/page/classtable/class_table_view/current_time_indicator.dart diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 9c8ddedb..78a77f33 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Feb 15 15:33:20 CST 2025 +#Thu Apr 16 09:43:18 CST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/assets/flutter_i18n/en_US.yaml b/assets/flutter_i18n/en_US.yaml index 22121a17..3757a4cc 100644 --- a/assets/flutter_i18n/en_US.yaml +++ b/assets/flutter_i18n/en_US.yaml @@ -159,6 +159,17 @@ classtable: output_to_system: "Export to system calendar" refresh_classtable: "Refresh schedule" switch_semester: "Switch classtable semester" + visual_settings: "Schedule visual settings" + visual_settings: + title: "Schedule visual settings" + current_time_section: "Current time indicator" + show_current_time_indicator: "Show current time indicator" + show_current_time_label: "Show mini time label" + completed_section: "Completed class colors" + completed_saturation_factor: "Fill saturation: {value}" + completed_text_saturation_factor: "Text saturation: {value}" + completed_border_alpha: "Border opacity: {value}" + completed_inner_alpha: "Fill opacity: {value}" status_source: class_table: "Class Table" exam: "Exams" diff --git a/assets/flutter_i18n/zh_CN.yaml b/assets/flutter_i18n/zh_CN.yaml index 65170e1c..b253955d 100644 --- a/assets/flutter_i18n/zh_CN.yaml +++ b/assets/flutter_i18n/zh_CN.yaml @@ -154,6 +154,17 @@ classtable: output_to_system: "导出到系统日历" refresh_classtable: "刷新日程表" switch_semester: "切换课程表学期" + visual_settings: "课表视觉设置" + visual_settings: + title: "课表视觉设置" + current_time_section: "当前时间标线" + show_current_time_indicator: "显示当前时间标线" + show_current_time_label: "显示迷你时间标签" + completed_section: "已完成课程颜色" + completed_saturation_factor: "底色饱和度: {value}" + completed_text_saturation_factor: "文字饱和度: {value}" + completed_border_alpha: "边框透明度: {value}" + completed_inner_alpha: "底色透明度: {value}" status_source: class_table: "课表" exam: "考试" diff --git a/assets/flutter_i18n/zh_TW.yaml b/assets/flutter_i18n/zh_TW.yaml index b51901b1..34d429cf 100644 --- a/assets/flutter_i18n/zh_TW.yaml +++ b/assets/flutter_i18n/zh_TW.yaml @@ -130,6 +130,17 @@ classtable: output_to_system: 導出到系統日曆 refresh_classtable: 刷新日程表 switch_semester: 切換課程表學期 + visual_settings: 課表視覺設置 + visual_settings: + title: 課表視覺設置 + current_time_section: 當前時間標線 + show_current_time_indicator: 顯示當前時間標線 + show_current_time_label: 顯示迷你時間標籤 + completed_section: 已完成課程顏色 + completed_saturation_factor: 底色飽和度: {value} + completed_text_saturation_factor: 文字飽和度: {value} + completed_border_alpha: 邊框透明度: {value} + completed_inner_alpha: 底色透明度: {value} 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 f23feb5b..1b6cab7c 100644 --- a/lib/page/classtable/class_page/content_classtable_page.dart +++ b/lib/page/classtable/class_page/content_classtable_page.dart @@ -16,6 +16,8 @@ import 'package:watermeter/page/classtable/class_add/class_add_window.dart'; import 'package:watermeter/page/classtable/class_page/class_change_list.dart'; import 'package:watermeter/page/classtable/class_page/classtable_inline_banner.dart'; import 'package:watermeter/page/classtable/class_table_view/class_table_view.dart'; +import 'package:watermeter/page/classtable/class_table_view/completed_class_style.dart'; +import 'package:watermeter/page/classtable/class_table_view/current_time_indicator.dart'; import 'package:watermeter/page/classtable/classtable_constant.dart'; import 'package:watermeter/page/classtable/classtable_state.dart'; import 'package:watermeter/page/classtable/class_page/not_arranged_class_list.dart'; @@ -249,6 +251,183 @@ class _ContentClassTablePageState extends State { ); } + Future _showClassTableVisualSettingsDialog() async { + var enabled = CurrentTimeIndicatorConfig.enabled; + var showTimeLabel = CurrentTimeIndicatorConfig.showTimeLabel; + var completedSaturationFactor = + CompletedClassStyleConfig.completedSaturationFactor; + var completedTextSaturationFactor = + CompletedClassStyleConfig.completedTextSaturationFactor; + var completedBorderAlpha = CompletedClassStyleConfig.completedBorderAlpha; + var completedInnerAlpha = CompletedClassStyleConfig.completedInnerAlpha; + + String formatPercent(double value) => "${(value * 100).round()}%"; + + final shouldApply = + await showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.title", + ), + ), + content: SizedBox( + width: 420, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.current_time_section", + ), + style: TextStyle(fontWeight: FontWeight.w700), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.show_current_time_indicator", + ), + ), + value: enabled, + onChanged: (value) => + setDialogState(() => enabled = value), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.show_current_time_label", + ), + ), + value: showTimeLabel, + onChanged: enabled + ? (value) => + setDialogState(() => showTimeLabel = value) + : null, + ), + const Divider(height: 24), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.completed_section", + ), + style: TextStyle(fontWeight: FontWeight.w700), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.completed_saturation_factor", + translationParams: { + "value": formatPercent(completedSaturationFactor), + }, + ), + ), + Slider( + value: completedSaturationFactor, + min: 0.1, + max: 1.0, + divisions: 18, + onChanged: (value) => setDialogState( + () => completedSaturationFactor = value, + ), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.completed_text_saturation_factor", + translationParams: { + "value": formatPercent( + completedTextSaturationFactor, + ), + }, + ), + ), + Slider( + value: completedTextSaturationFactor, + min: 0.1, + max: 1.0, + divisions: 18, + onChanged: (value) => setDialogState( + () => completedTextSaturationFactor = value, + ), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.completed_border_alpha", + translationParams: { + "value": formatPercent(completedBorderAlpha), + }, + ), + ), + Slider( + value: completedBorderAlpha, + min: 0.1, + max: 1.0, + divisions: 18, + onChanged: (value) => + setDialogState(() => completedBorderAlpha = value), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.completed_inner_alpha", + translationParams: { + "value": formatPercent(completedInnerAlpha), + }, + ), + ), + Slider( + value: completedInnerAlpha, + min: 0.1, + max: 1.0, + divisions: 18, + onChanged: (value) => + setDialogState(() => completedInnerAlpha = value), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(FlutterI18n.translate(context, "cancel")), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(FlutterI18n.translate(context, "confirm")), + ), + ], + ), + ), + ) ?? + false; + + if (!shouldApply || !mounted) { + return; + } + + CurrentTimeIndicatorConfig.enabled = enabled; + CurrentTimeIndicatorConfig.showTimeLabel = showTimeLabel; + CompletedClassStyleConfig.completedSaturationFactor = + completedSaturationFactor; + CompletedClassStyleConfig.completedTextSaturationFactor = + completedTextSaturationFactor; + CompletedClassStyleConfig.completedBorderAlpha = completedBorderAlpha; + CompletedClassStyleConfig.completedInnerAlpha = completedInnerAlpha; + setState(() {}); + } + @override Widget build(BuildContext context) { final state = ClassTableState.of(context)!.controllers; @@ -327,6 +506,15 @@ class _ContentClassTablePageState extends State { ), ), ), + PopupMenuItem( + value: 'J', + child: Text( + FlutterI18n.translate( + context, + "classtable.popup_menu.visual_settings", + ), + ), + ), ], onSelected: (String action) async { final box = context.findRenderObject() as RenderBox?; @@ -573,6 +761,10 @@ class _ContentClassTablePageState extends State { } }); } + break; + case 'J': + await _showClassTableVisualSettingsDialog(); + break; } }, ), diff --git a/lib/page/classtable/class_table_view/class_card.dart b/lib/page/classtable/class_table_view/class_card.dart index 59dbe0a7..b5e4e987 100644 --- a/lib/page/classtable/class_table_view/class_card.dart +++ b/lib/page/classtable/class_table_view/class_card.dart @@ -10,6 +10,7 @@ import 'package:watermeter/model/xidian_ids/exam.dart'; import 'package:watermeter/model/xidian_ids/experiment.dart'; import 'package:watermeter/page/classtable/class_add/class_add_window.dart'; import 'package:watermeter/page/classtable/class_table_view/class_organized_data.dart'; +import 'package:watermeter/page/classtable/class_table_view/completed_class_style.dart'; import 'package:watermeter/page/classtable/arrangement_detail/arrangement_detail.dart'; import 'package:watermeter/page/classtable/classtable_state.dart'; import 'package:watermeter/page/public_widget/both_side_sheet.dart'; @@ -18,18 +19,23 @@ import 'package:watermeter/page/public_widget/public_widget.dart'; /// The card in [classSubRow], metioned in [ClassTableView]. class ClassCard extends StatelessWidget { final ClassOrgainzedData detail; + final bool isCompleted; List get data => detail.data; MaterialColor get color => detail.color; String get name => detail.name; String? get place => detail.place; - const ClassCard({super.key, required this.detail}); + const ClassCard({super.key, required this.detail, required this.isCompleted}); @override Widget build(BuildContext context) { ClassTableWidgetState classTableState = ClassTableState.of( context, )!.controllers; + final style = CompletedClassStyle.resolve( + palette: color, + isCompleted: isCompleted, + ); /// This is the result of the class info card. return Padding( @@ -39,7 +45,7 @@ class ClassCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), child: Container( // Border - color: color.shade300.withValues(alpha: 0.8), + color: style.borderColor.withValues(alpha: style.borderAlpha), padding: const EdgeInsets.all(2), child: Stack( children: [ @@ -47,7 +53,7 @@ class ClassCard extends StatelessWidget { // Inner borderRadius: BorderRadius.circular(6), child: Container( - color: color.shade100.withValues(alpha: 0.7), + color: style.innerColor.withValues(alpha: style.innerAlpha), child: TextButton( style: TextButton.styleFrom( padding: EdgeInsets.zero, @@ -117,20 +123,14 @@ class ClassCard extends StatelessWidget { Flexible( child: Text( name, - style: TextStyle( - color: color.shade900, - fontSize: isPhone(context) ? 12 : 14, - ), + style: TextStyle(color: style.textColor, fontSize: isPhone(context) ? 12 : 14), maxLines: 3, overflow: TextOverflow.clip, ), ), Text( "@${place ?? FlutterI18n.translate(context, "classtable.class_card.unknown_classroom")}", - style: TextStyle( - color: color.shade900, - fontSize: isPhone(context) ? 10 : 12, - ), + style: TextStyle(color: style.textColor, fontSize: isPhone(context) ? 10 : 12), ), if (data.length > 1) Text( @@ -142,10 +142,7 @@ class ClassCard extends StatelessWidget { .toString(), }, ), - style: TextStyle( - color: color.shade900, - fontSize: isPhone(context) ? 10 : 12, - ), + style: TextStyle(color: style.textColor, fontSize: isPhone(context) ? 10 : 12), ), ], ) @@ -161,7 +158,7 @@ class ClassCard extends StatelessWidget { ClipPath( clipper: Triangle(), child: Container( - color: color.shade300, + color: style.borderColor, ).constrained(width: 8, height: 8), ).alignment(Alignment.topRight), ], 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..bc4ad6e7 100644 --- a/lib/page/classtable/class_table_view/class_organized_data.dart +++ b/lib/page/classtable/class_table_view/class_organized_data.dart @@ -29,6 +29,7 @@ class ClassOrgainzedData { final String name; final String? place; + final DateTime? actualEndTime; final MaterialColor color; @@ -80,6 +81,7 @@ class ClassOrgainzedData { color: color, name: name, place: timeArrangement.classroom, + actualEndTime: null, ); } @@ -96,6 +98,7 @@ class ClassOrgainzedData { place: "${subject.place} " "${subject.seat == null ? "" : "${subject.seat}"}", + actualEndTime: subject.stopTime, ); factory ClassOrgainzedData.fromExperiment( @@ -110,6 +113,7 @@ class ClassOrgainzedData { color: color, name: exp.name, place: exp.classroom, + actualEndTime: stop, ); ClassOrgainzedData({ @@ -119,6 +123,7 @@ class ClassOrgainzedData { required this.name, required this.color, this.place, + this.actualEndTime, }); static double _transferIndex(DateTime time) { @@ -169,6 +174,7 @@ class ClassOrgainzedData { required this.color, required this.name, this.place, + this.actualEndTime, }) { 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..6cce02ca 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -8,8 +8,10 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:watermeter/model/time_list.dart'; import 'package:watermeter/page/classtable/class_table_view/class_card.dart'; +import 'package:watermeter/page/classtable/class_table_view/completed_class_style.dart'; import 'package:watermeter/page/classtable/class_table_view/class_organized_data.dart'; import 'package:watermeter/page/classtable/class_table_view/classtable_date_row.dart'; +import 'package:watermeter/page/classtable/class_table_view/current_time_indicator.dart'; import 'package:watermeter/page/classtable/classtable_constant.dart'; import 'package:watermeter/page/classtable/classtable_state.dart'; import 'package:watermeter/page/public_widget/public_widget.dart'; @@ -43,6 +45,29 @@ class _ClassTableViewState extends State { late ClassTableWidgetState classTableState; late BoxConstraints size; + DateTime get _visibleWeekStart => classTableState.startDay + .add(Duration(days: 7 * classTableState.offset)) + .add(Duration(days: 7 * widget.index)); + + bool _isCompleted(ClassOrgainzedData data, int dayIndex) { + final now = classTableState.currentTime; + final dayStart = _visibleWeekStart.add(Duration(days: dayIndex - 1)); + return CompletedClassStyle.isCompleted( + data: data, + now: now, + dayStart: dayStart, + ); + } + + Positioned? _currentTimeIndicator() => CurrentTimeIndicator.build( + context: context, + now: classTableState.currentTime, + weekStart: _visibleWeekStart, + leftRow: leftRow, + blockWidth: blockwidth, + blockHeight: blockheight, + ); + /// The height of the class card. double blockheight(double count) => count * @@ -62,18 +87,24 @@ class _ClassTableViewState extends State { /// Choice the day and render it! for (var i in arrangedEvents) { /// Generate the row. + final isCompleted = _isCompleted(i, index); thisRow.add( Positioned( top: blockheight(i.start), height: blockheight(i.stop - i.start), left: leftRow + blockwidth * (index - 1), width: blockwidth, - child: ClassCard(detail: i), + child: ClassCard(detail: i, isCompleted: isCompleted), ), ); } } + final timeIndicator = _currentTimeIndicator(); + if (timeIndicator != null) { + thisRow.add(timeIndicator); + } + if (thisRow.isEmpty && !preference.getBool(preference.Preference.decorated)) { thisRow.add( diff --git a/lib/page/classtable/class_table_view/completed_class_style.dart b/lib/page/classtable/class_table_view/completed_class_style.dart new file mode 100644 index 00000000..3813272d --- /dev/null +++ b/lib/page/classtable/class_table_view/completed_class_style.dart @@ -0,0 +1,113 @@ +// Copyright 2025 Traintime PDA authors. +// SPDX-License-Identifier: MPL-2.0 OR Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:watermeter/model/time_list.dart'; +import 'package:watermeter/model/xidian_ids/classtable.dart'; +import 'package:watermeter/page/classtable/class_table_view/class_organized_data.dart'; + +class CompletedClassStyleConfig { + /// Completed-card color tuning. + /// Lower values make finished classes look more muted and faded. + static double completedSaturationFactor = 0.35; + static double completedTextSaturationFactor = 0.55; + static double completedBorderAlpha = 0.55; + static double completedInnerAlpha = 0.45; + + /// Active-card baseline appearance. + static double activeBorderAlpha = 0.8; + static double activeInnerAlpha = 0.7; +} + +class CompletedClassStyleData { + final Color borderColor; + final Color innerColor; + final Color textColor; + final double borderAlpha; + final double innerAlpha; + + const CompletedClassStyleData({ + required this.borderColor, + required this.innerColor, + required this.textColor, + required this.borderAlpha, + required this.innerAlpha, + }); +} + +class CompletedClassStyle { + static Color _desaturateColor(Color color, {double factor = 0.35}) { + final hsl = HSLColor.fromColor(color); + return hsl.withSaturation(hsl.saturation * factor).toColor(); + } + + static CompletedClassStyleData resolve({ + required MaterialColor palette, + required bool isCompleted, + }) { + final borderColor = isCompleted + ? _desaturateColor( + palette.shade300, + factor: CompletedClassStyleConfig.completedSaturationFactor, + ) + : palette.shade300; + final innerColor = isCompleted + ? _desaturateColor( + palette.shade100, + factor: CompletedClassStyleConfig.completedSaturationFactor, + ) + : palette.shade100; + final textColor = isCompleted + ? _desaturateColor( + palette.shade900, + factor: CompletedClassStyleConfig.completedTextSaturationFactor, + ) + : palette.shade900; + + return CompletedClassStyleData( + borderColor: borderColor, + innerColor: innerColor, + textColor: textColor, + borderAlpha: isCompleted + ? CompletedClassStyleConfig.completedBorderAlpha + : CompletedClassStyleConfig.activeBorderAlpha, + innerAlpha: isCompleted + ? CompletedClassStyleConfig.completedInnerAlpha + : CompletedClassStyleConfig.activeInnerAlpha, + ); + } + + static DateTime eventEndTime({ + required ClassOrgainzedData data, + required DateTime dayStart, + }) { + if (data.actualEndTime != null) { + return data.actualEndTime!; + } + + if (data.data.isEmpty || data.data.first is! TimeArrangement) { + return dayStart; + } + + final arrangement = data.data.first as TimeArrangement; + final stopIndex = (arrangement.stop - 1) * 2 + 1; + final stopTime = timeList[stopIndex].split(':'); + + return DateTime( + dayStart.year, + dayStart.month, + dayStart.day, + int.parse(stopTime[0]), + int.parse(stopTime[1]), + ); + } + + static bool isCompleted({ + required ClassOrgainzedData data, + required DateTime now, + required DateTime dayStart, + }) { + final end = eventEndTime(data: data, dayStart: dayStart); + return end.isBefore(now); + } +} diff --git a/lib/page/classtable/class_table_view/current_time_indicator.dart b/lib/page/classtable/class_table_view/current_time_indicator.dart new file mode 100644 index 00000000..3cdc650b --- /dev/null +++ b/lib/page/classtable/class_table_view/current_time_indicator.dart @@ -0,0 +1,137 @@ +// Copyright 2025 Traintime PDA authors. +// SPDX-License-Identifier: MPL-2.0 OR Apache-2.0 + +import 'package:flutter/material.dart'; + +class CurrentTimeIndicatorConfig { + static bool enabled = true; + static bool showTimeLabel = true; + static double lineAlpha = 0.9; + static double lineThickness = 2; + static double labelHeight = 14; + static double labelFontSize = 9; +} + +class CurrentTimeIndicator { + static const List _timeInBlock = [ + '08:30', + '09:20', + '10:25', + '11:15', + '12:00', + '14:00', + '14:50', + '15:55', + '16:45', + '17:30', + '19:00', + '19:55', + '20:35', + '21:25', + ]; + + static double _transferIndex(DateTime time) { + final timeInMin = time.hour * 60 + time.minute; + var previous = 0; + for (final i in _timeInBlock) { + final timeChosen = + int.parse(i.split(':')[0]) * 60 + int.parse(i.split(':')[1]); + if (previous == 0) { + if (timeInMin < timeChosen) { + return 0; + } + previous = timeChosen; + continue; + } + + if (timeInMin >= previous && timeInMin < timeChosen) { + var basic = 0.0; + var blocks = 5.0; + final ratio = (timeInMin - previous) / (timeChosen - previous); + if (previous < 12 * 60) { + basic = (_timeInBlock.indexOf(i) - 1) * 5; + } else if (previous < 14 * 60) { + basic = 20; + blocks = 3; + } else if (previous < 17.5 * 60) { + basic = 23 + (_timeInBlock.indexOf(i) - 6) * 5; + } else if (previous < 19 * 60) { + basic = 43; + blocks = 3; + } else { + basic = 46 + (_timeInBlock.indexOf(i) - 11) * 5; + } + return basic + blocks * ratio; + } + previous = timeChosen; + } + + return 61; + } + + static Positioned? build({ + required BuildContext context, + required DateTime now, + required DateTime weekStart, + required double leftRow, + required double blockWidth, + required double Function(double) blockHeight, + }) { + if (!CurrentTimeIndicatorConfig.enabled) { + return null; + } + + final today = DateTime(now.year, now.month, now.day); + final normalizedWeekStart = + DateTime(weekStart.year, weekStart.month, weekStart.day); + final dayOffset = today.difference(normalizedWeekStart).inDays; + if (dayOffset < 0 || dayOffset > 6) { + return null; + } + + final lineTop = blockHeight(_transferIndex(now)); + final color = Theme.of(context).colorScheme.primary; + final labelText = + '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; + final hasLabel = CurrentTimeIndicatorConfig.showTimeLabel; + final labelHeight = hasLabel ? CurrentTimeIndicatorConfig.labelHeight : 0.0; + final labelTop = lineTop > labelHeight ? lineTop - labelHeight : 0.0; + final lineOffset = lineTop - labelTop; + + return Positioned( + left: leftRow + blockWidth * dayOffset, + top: labelTop, + width: blockWidth, + child: IgnorePointer( + child: SizedBox( + height: lineOffset + CurrentTimeIndicatorConfig.lineThickness, + child: Stack( + children: [ + if (hasLabel) + Align( + alignment: Alignment.topCenter, + child: Text( + labelText, + style: TextStyle( + fontSize: CurrentTimeIndicatorConfig.labelFontSize, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ), + Positioned( + top: lineOffset, + left: 0, + right: 0, + child: Container( + height: CurrentTimeIndicatorConfig.lineThickness, + color: color.withValues(alpha: CurrentTimeIndicatorConfig.lineAlpha), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/page/classtable/classtable_state.dart b/lib/page/classtable/classtable_state.dart index 3e40952c..2e4c366f 100644 --- a/lib/page/classtable/classtable_state.dart +++ b/lib/page/classtable/classtable_state.dart @@ -12,6 +12,7 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:watermeter/controller/classtable_controller.dart'; import 'package:watermeter/controller/exam_controller.dart'; +import 'package:watermeter/controller/global_timer_controller.dart'; import 'package:watermeter/controller/physics_experiment_controller.dart'; import 'package:watermeter/controller/other_experiment_controller.dart'; import 'package:watermeter/controller/week_swift_controller.dart'; @@ -112,6 +113,7 @@ class ClassTableWidgetState with ChangeNotifier { otherExperimentController.isOtherExperimentFromCache.value; otherExperimentController.otherExperimentCacheHintKey.value; weekSwiftController.weekSwiftSignal.value; + GlobalTimerController.i.currentTimeSignal.value; notifyListeners(); }, debugLabel: "ClassTableWidgetStateSignalBridgeEffect"), ); @@ -266,6 +268,8 @@ class ClassTableWidgetState with ChangeNotifier { /// The currentWeek. final int currentWeek; + DateTime get currentTime => GlobalTimerController.i.currentTimeSignal.value; + /// The exam list. List get subjects => examController.subjects.value; diff --git a/pubspec.lock b/pubspec.lock index 7919ecf1..a64d7540 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -777,10 +777,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -1422,10 +1422,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" tflite_flutter: dependency: "direct main" description: From 375786392cb78cddbf0e70111804f3017d6556f7 Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Thu, 16 Apr 2026 23:23:32 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E6=97=A5=E6=9C=9F=E5=88=97=E6=96=B9=E6=A1=86=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=BC=BA=E8=AF=BE?= =?UTF-8?q?=E8=A1=A8=E8=A7=86=E8=A7=89=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/flutter_i18n/zh_CN.yaml | 1 + .../class_page/content_classtable_page.dart | 29 ++++++-- .../class_table_view/class_table_view.dart | 13 ++++ .../current_time_indicator.dart | 67 +++++++++++++++++-- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/assets/flutter_i18n/zh_CN.yaml b/assets/flutter_i18n/zh_CN.yaml index b253955d..5bee487a 100644 --- a/assets/flutter_i18n/zh_CN.yaml +++ b/assets/flutter_i18n/zh_CN.yaml @@ -160,6 +160,7 @@ classtable: current_time_section: "当前时间标线" show_current_time_indicator: "显示当前时间标线" show_current_time_label: "显示迷你时间标签" + show_current_day_column_box: "显示当前日期列方框" completed_section: "已完成课程颜色" completed_saturation_factor: "底色饱和度: {value}" completed_text_saturation_factor: "文字饱和度: {value}" diff --git a/lib/page/classtable/class_page/content_classtable_page.dart b/lib/page/classtable/class_page/content_classtable_page.dart index 1b6cab7c..b8165901 100644 --- a/lib/page/classtable/class_page/content_classtable_page.dart +++ b/lib/page/classtable/class_page/content_classtable_page.dart @@ -254,6 +254,8 @@ class _ContentClassTablePageState extends State { Future _showClassTableVisualSettingsDialog() async { var enabled = CurrentTimeIndicatorConfig.enabled; var showTimeLabel = CurrentTimeIndicatorConfig.showTimeLabel; + var showCurrentDayColumnBox = + CurrentTimeIndicatorConfig.showCurrentDayColumnBox; var completedSaturationFactor = CompletedClassStyleConfig.completedSaturationFactor; var completedTextSaturationFactor = @@ -314,6 +316,19 @@ class _ContentClassTablePageState extends State { setDialogState(() => showTimeLabel = value) : null, ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.show_current_day_column_box", + ), + ), + value: showCurrentDayColumnBox, + onChanged: (value) => setDialogState( + () => showCurrentDayColumnBox = value, + ), + ), const Divider(height: 24), Text( FlutterI18n.translate( @@ -419,12 +434,14 @@ class _ContentClassTablePageState extends State { CurrentTimeIndicatorConfig.enabled = enabled; CurrentTimeIndicatorConfig.showTimeLabel = showTimeLabel; - CompletedClassStyleConfig.completedSaturationFactor = - completedSaturationFactor; - CompletedClassStyleConfig.completedTextSaturationFactor = - completedTextSaturationFactor; - CompletedClassStyleConfig.completedBorderAlpha = completedBorderAlpha; - CompletedClassStyleConfig.completedInnerAlpha = completedInnerAlpha; + CurrentTimeIndicatorConfig.showCurrentDayColumnBox = + showCurrentDayColumnBox; + CompletedClassStyleConfig.completedSaturationFactor = + completedSaturationFactor; + CompletedClassStyleConfig.completedTextSaturationFactor = + completedTextSaturationFactor; + CompletedClassStyleConfig.completedBorderAlpha = completedBorderAlpha; + CompletedClassStyleConfig.completedInnerAlpha = completedInnerAlpha; setState(() {}); } 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 6cce02ca..0560e0b6 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -68,6 +68,15 @@ class _ClassTableViewState extends State { blockHeight: blockheight, ); + Positioned? _currentDayColumnBox() => CurrentTimeIndicator.buildDayColumnBox( + context: context, + now: classTableState.currentTime, + weekStart: _visibleWeekStart, + leftRow: leftRow, + blockWidth: blockwidth, + blockHeight: blockheight, + ); + /// The height of the class card. double blockheight(double count) => count * @@ -101,6 +110,10 @@ class _ClassTableViewState extends State { } final timeIndicator = _currentTimeIndicator(); + final currentDayColumnBox = _currentDayColumnBox(); + if (currentDayColumnBox != null) { + thisRow.add(currentDayColumnBox); + } if (timeIndicator != null) { thisRow.add(timeIndicator); } diff --git a/lib/page/classtable/class_table_view/current_time_indicator.dart b/lib/page/classtable/class_table_view/current_time_indicator.dart index 3cdc650b..9a76d209 100644 --- a/lib/page/classtable/class_table_view/current_time_indicator.dart +++ b/lib/page/classtable/class_table_view/current_time_indicator.dart @@ -6,10 +6,13 @@ import 'package:flutter/material.dart'; class CurrentTimeIndicatorConfig { static bool enabled = true; static bool showTimeLabel = true; + static bool showCurrentDayColumnBox = true; static double lineAlpha = 0.9; static double lineThickness = 2; static double labelHeight = 14; static double labelFontSize = 9; + static double dayColumnBorderAlpha = 0.65; + static double dayColumnBorderWidth = 2; } class CurrentTimeIndicator { @@ -82,8 +85,11 @@ class CurrentTimeIndicator { } final today = DateTime(now.year, now.month, now.day); - final normalizedWeekStart = - DateTime(weekStart.year, weekStart.month, weekStart.day); + final normalizedWeekStart = DateTime( + weekStart.year, + weekStart.month, + weekStart.day, + ); final dayOffset = today.difference(normalizedWeekStart).inDays; if (dayOffset < 0 || dayOffset > 6) { return null; @@ -91,6 +97,9 @@ class CurrentTimeIndicator { final lineTop = blockHeight(_transferIndex(now)); final color = Theme.of(context).colorScheme.primary; + final lineHorizontalInset = CurrentTimeIndicatorConfig.showCurrentDayColumnBox + ? CurrentTimeIndicatorConfig.dayColumnBorderWidth + : 0.0; final labelText = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; final hasLabel = CurrentTimeIndicatorConfig.showTimeLabel; @@ -121,11 +130,13 @@ class CurrentTimeIndicator { ), Positioned( top: lineOffset, - left: 0, - right: 0, + left: lineHorizontalInset / 2, + right: lineHorizontalInset / 2, child: Container( height: CurrentTimeIndicatorConfig.lineThickness, - color: color.withValues(alpha: CurrentTimeIndicatorConfig.lineAlpha), + color: color.withValues( + alpha: CurrentTimeIndicatorConfig.lineAlpha, + ), ), ), ], @@ -134,4 +145,50 @@ class CurrentTimeIndicator { ), ); } + + static Positioned? buildDayColumnBox({ + required BuildContext context, + required DateTime now, + required DateTime weekStart, + required double leftRow, + required double blockWidth, + required double Function(double) blockHeight, + }) { + if (!CurrentTimeIndicatorConfig.showCurrentDayColumnBox) { + return null; + } + + final today = DateTime(now.year, now.month, now.day); + final normalizedWeekStart = DateTime( + weekStart.year, + weekStart.month, + weekStart.day, + ); + final dayOffset = today.difference(normalizedWeekStart).inDays; + if (dayOffset < 0 || dayOffset > 6) { + return null; + } + + final color = Theme.of(context).colorScheme.primary.withValues( + alpha: CurrentTimeIndicatorConfig.lineAlpha, + ); + + return Positioned( + left: leftRow + blockWidth * dayOffset - + CurrentTimeIndicatorConfig.dayColumnBorderWidth / 2, + top: 0, + width: blockWidth + CurrentTimeIndicatorConfig.dayColumnBorderWidth, + height: blockHeight(61), + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: color, + width: CurrentTimeIndicatorConfig.dayColumnBorderWidth, + ), + ), + ), + ), + ); + } } From 841ec0ed05d8a1c80c057a2705f2baf54ceae5db Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Fri, 17 Apr 2026 17:23:06 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AF=BE?= =?UTF-8?q?=E8=A1=A8=E8=A7=86=E8=A7=89=E8=AE=BE=E7=BD=AE=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=97=B6=E9=97=B4=E6=8C=87=E7=A4=BA=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E6=8C=87=E7=A4=BA=E5=99=A8=E7=AD=89=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=A0=B7=E5=BC=8F=E8=BF=9B=E8=A1=8C=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/flutter_i18n/en_US.yaml | 12 +- assets/flutter_i18n/zh_CN.yaml | 17 +- assets/flutter_i18n/zh_TW.yaml | 16 +- lib/controller/global_timer_controller.dart | 2 +- lib/controller/schoolnet_controller.dart | 2 +- .../class_page/classtable_page.dart | 20 +- .../class_page/content_classtable_page.dart | 87 +++++++-- .../class_table_view/class_card.dart | 184 +++++++++++------- .../class_organized_data.dart | 100 +++++----- .../class_table_view/class_table_view.dart | 43 ++-- .../current_time_indicator.dart | 114 ++++++----- 11 files changed, 385 insertions(+), 212 deletions(-) diff --git a/assets/flutter_i18n/en_US.yaml b/assets/flutter_i18n/en_US.yaml index 3757a4cc..86f801d1 100644 --- a/assets/flutter_i18n/en_US.yaml +++ b/assets/flutter_i18n/en_US.yaml @@ -159,13 +159,17 @@ classtable: output_to_system: "Export to system calendar" refresh_classtable: "Refresh schedule" switch_semester: "Switch classtable semester" - visual_settings: "Schedule visual settings" + visual_settings: "Schedule appearance settings" visual_settings: - title: "Schedule visual settings" - current_time_section: "Current time indicator" + title: "Schedule appearance settings" + current_time_section: "Time indicators" show_current_time_indicator: "Show current time indicator" show_current_time_label: "Show mini time label" - completed_section: "Completed class colors" + show_today_column_highlight: "Highlight today's column" + unfinished_section: "Unfinished class style" + active_border_alpha: "Border opacity: {value}" + active_inner_alpha: "Fill opacity: {value}" + completed_section: "Completed class style" completed_saturation_factor: "Fill saturation: {value}" completed_text_saturation_factor: "Text saturation: {value}" completed_border_alpha: "Border opacity: {value}" diff --git a/assets/flutter_i18n/zh_CN.yaml b/assets/flutter_i18n/zh_CN.yaml index 5bee487a..e7f663f8 100644 --- a/assets/flutter_i18n/zh_CN.yaml +++ b/assets/flutter_i18n/zh_CN.yaml @@ -154,14 +154,17 @@ classtable: output_to_system: "导出到系统日历" refresh_classtable: "刷新日程表" switch_semester: "切换课程表学期" - visual_settings: "课表视觉设置" + visual_settings: "课表外观设置" visual_settings: - title: "课表视觉设置" - current_time_section: "当前时间标线" - show_current_time_indicator: "显示当前时间标线" - show_current_time_label: "显示迷你时间标签" - show_current_day_column_box: "显示当前日期列方框" - completed_section: "已完成课程颜色" + title: "课表外观设置" + current_time_section: "时间指示" + show_current_time_indicator: "显示当前时间指示线" + show_current_time_label: "显示迷你数字时钟" + show_today_column_highlight: "强调显示今天的纵列" + unfinished_section: "未完成课程样式" + active_border_alpha: "边框透明度: {value}" + active_inner_alpha: "底色透明度: {value}" + completed_section: "已完成课程样式" completed_saturation_factor: "底色饱和度: {value}" completed_text_saturation_factor: "文字饱和度: {value}" completed_border_alpha: "边框透明度: {value}" diff --git a/assets/flutter_i18n/zh_TW.yaml b/assets/flutter_i18n/zh_TW.yaml index 34d429cf..47320f3d 100644 --- a/assets/flutter_i18n/zh_TW.yaml +++ b/assets/flutter_i18n/zh_TW.yaml @@ -130,13 +130,17 @@ classtable: output_to_system: 導出到系統日曆 refresh_classtable: 刷新日程表 switch_semester: 切換課程表學期 - visual_settings: 課表視覺設置 + visual_settings: 課表外觀設置 visual_settings: - title: 課表視覺設置 - current_time_section: 當前時間標線 - show_current_time_indicator: 顯示當前時間標線 - show_current_time_label: 顯示迷你時間標籤 - completed_section: 已完成課程顏色 + title: 課表外觀設置 + current_time_section: 時間指示 + show_current_time_indicator: 顯示當前時間指示線 + show_current_time_label: 顯示迷你数字時钟 + show_today_column_highlight: 强调顯示今天的纵列 + unfinished_section: 未完成課程樣式 + active_border_alpha: 邊框透明度: {value} + active_inner_alpha: 底色透明度: {value} + completed_section: 已完成課程樣式 completed_saturation_factor: 底色飽和度: {value} completed_text_saturation_factor: 文字飽和度: {value} completed_border_alpha: 邊框透明度: {value} diff --git a/lib/controller/global_timer_controller.dart b/lib/controller/global_timer_controller.dart index 708ec506..732d3d06 100644 --- a/lib/controller/global_timer_controller.dart +++ b/lib/controller/global_timer_controller.dart @@ -9,7 +9,7 @@ import 'package:watermeter/repository/logger.dart'; class GlobalTimerController { static final GlobalTimerController i = GlobalTimerController._(); GlobalTimerController._() { - _timer = Timer.periodic(const Duration(minutes: 1), (_) { + _timer = Timer.periodic(const Duration(seconds: 15), (_) { currentTimeSignal.value = DateTime.now(); log.debug("Global Timer: Time is ${currentTimeSignal.value}"); }); diff --git a/lib/controller/schoolnet_controller.dart b/lib/controller/schoolnet_controller.dart index 3c4d6456..10cb0a2f 100644 --- a/lib/controller/schoolnet_controller.dart +++ b/lib/controller/schoolnet_controller.dart @@ -29,7 +29,7 @@ class SchoolnetController { if (schoolNetUsageSignal.value.isLoading) return; _captchaFunction = captchaFunction; - await schoolNetUsageSignal.refresh().catchError( + await schoolNetUsageSignal.reload().catchError( (e, s) => log.handle( e, s, diff --git a/lib/page/classtable/class_page/classtable_page.dart b/lib/page/classtable/class_page/classtable_page.dart index 0a8980ca..46025cea 100644 --- a/lib/page/classtable/class_page/classtable_page.dart +++ b/lib/page/classtable/class_page/classtable_page.dart @@ -12,19 +12,31 @@ class ClassTablePage extends StatefulWidget { class _ClassTablePageState extends State { late ClassTableWidgetState classTableState; + ClassTableWidgetState? _attachedClassTableState; - void _reload() => setState(() {}); + void _reload() { + if (mounted) { + setState(() {}); + } + } @override void didChangeDependencies() { super.didChangeDependencies(); - classTableState = ClassTableState.of(context)!.controllers; - classTableState.addListener(_reload); + final nextClassTableState = ClassTableState.of(context)!.controllers; + + if (_attachedClassTableState != nextClassTableState) { + _attachedClassTableState?.removeListener(_reload); + _attachedClassTableState = nextClassTableState; + _attachedClassTableState!.addListener(_reload); + } + + classTableState = nextClassTableState; } @override void dispose() { - classTableState.removeListener(_reload); + _attachedClassTableState?.removeListener(_reload); classTableState.dispose(); super.dispose(); } diff --git a/lib/page/classtable/class_page/content_classtable_page.dart b/lib/page/classtable/class_page/content_classtable_page.dart index b8165901..1bf0bb60 100644 --- a/lib/page/classtable/class_page/content_classtable_page.dart +++ b/lib/page/classtable/class_page/content_classtable_page.dart @@ -50,8 +50,12 @@ class _ContentClassTablePageState extends State { late BoxDecoration decoration; late ClassTableWidgetState classTableState; + ClassTableWidgetState? _attachedClassTableState; void _switchPage() { + if (!mounted) { + return; + } setState(() => isTopRowLocked = true); Future.wait([ rowControl.animateToPage( @@ -64,18 +68,30 @@ class _ContentClassTablePageState extends State { curve: Curves.easeInOutCubic, duration: const Duration(milliseconds: changePageTime), ), - ]).then((value) => isTopRowLocked = false); + ]).then((value) { + if (mounted) { + isTopRowLocked = false; + } + }); } @override void dispose() { - classTableState.removeListener(_switchPage); + _attachedClassTableState?.removeListener(_switchPage); super.dispose(); } @override void didChangeDependencies() { - classTableState = ClassTableState.of(context)!.controllers; + final nextClassTableState = ClassTableState.of(context)!.controllers; + + if (_attachedClassTableState != nextClassTableState) { + _attachedClassTableState?.removeListener(_switchPage); + _attachedClassTableState = nextClassTableState; + _attachedClassTableState!.addListener(_switchPage); + } + + classTableState = nextClassTableState; pageControl = PageController( initialPage: classTableState.chosenWeek, @@ -93,11 +109,6 @@ class _ContentClassTablePageState extends State { ); /// Let controllers listen to the currentWeek's change. - // if (isPushedListener == false) { - classTableState.addListener(_switchPage); - // isPushedListener = true; - //} - /// Init the background. File image = File("${supportPath.path}/${classTableState.decorationName}"); decoration = BoxDecoration( @@ -254,8 +265,10 @@ class _ContentClassTablePageState extends State { Future _showClassTableVisualSettingsDialog() async { var enabled = CurrentTimeIndicatorConfig.enabled; var showTimeLabel = CurrentTimeIndicatorConfig.showTimeLabel; - var showCurrentDayColumnBox = - CurrentTimeIndicatorConfig.showCurrentDayColumnBox; + var showTodayColumnHighlight = + CurrentTimeIndicatorConfig.showTodayColumnHighlight; + var activeBorderAlpha = CompletedClassStyleConfig.activeBorderAlpha; + var activeInnerAlpha = CompletedClassStyleConfig.activeInnerAlpha; var completedSaturationFactor = CompletedClassStyleConfig.completedSaturationFactor; var completedTextSaturationFactor = @@ -321,14 +334,56 @@ class _ContentClassTablePageState extends State { title: Text( FlutterI18n.translate( context, - "classtable.visual_settings.show_current_day_column_box", + "classtable.visual_settings.show_today_column_highlight", ), ), - value: showCurrentDayColumnBox, + value: showTodayColumnHighlight, onChanged: (value) => setDialogState( - () => showCurrentDayColumnBox = value, + () => showTodayColumnHighlight = value, + ), + ), + const Divider(height: 24), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.unfinished_section", + ), + style: TextStyle(fontWeight: FontWeight.w700), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.active_border_alpha", + translationParams: { + "value": formatPercent(activeBorderAlpha), + }, + ), + ), + Slider( + value: activeBorderAlpha, + min: 0.1, + max: 1.0, + divisions: 18, + onChanged: (value) => + setDialogState(() => activeBorderAlpha = value), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.active_inner_alpha", + translationParams: { + "value": formatPercent(activeInnerAlpha), + }, ), ), + Slider( + value: activeInnerAlpha, + min: 0.1, + max: 1.0, + divisions: 18, + onChanged: (value) => + setDialogState(() => activeInnerAlpha = value), + ), const Divider(height: 24), Text( FlutterI18n.translate( @@ -434,8 +489,10 @@ class _ContentClassTablePageState extends State { CurrentTimeIndicatorConfig.enabled = enabled; CurrentTimeIndicatorConfig.showTimeLabel = showTimeLabel; - CurrentTimeIndicatorConfig.showCurrentDayColumnBox = - showCurrentDayColumnBox; + CurrentTimeIndicatorConfig.showTodayColumnHighlight = + showTodayColumnHighlight; + CompletedClassStyleConfig.activeBorderAlpha = activeBorderAlpha; + CompletedClassStyleConfig.activeInnerAlpha = activeInnerAlpha; CompletedClassStyleConfig.completedSaturationFactor = completedSaturationFactor; CompletedClassStyleConfig.completedTextSaturationFactor = diff --git a/lib/page/classtable/class_table_view/class_card.dart b/lib/page/classtable/class_table_view/class_card.dart index b5e4e987..3a9e494d 100644 --- a/lib/page/classtable/class_table_view/class_card.dart +++ b/lib/page/classtable/class_table_view/class_card.dart @@ -19,48 +19,79 @@ import 'package:watermeter/page/public_widget/public_widget.dart'; /// The card in [classSubRow], metioned in [ClassTableView]. class ClassCard extends StatelessWidget { final ClassOrgainzedData detail; - final bool isCompleted; + final double completedHeight; List get data => detail.data; MaterialColor get color => detail.color; String get name => detail.name; String? get place => detail.place; - const ClassCard({super.key, required this.detail, required this.isCompleted}); + const ClassCard({ + super.key, + required this.detail, + required this.completedHeight, + }); @override Widget build(BuildContext context) { ClassTableWidgetState classTableState = ClassTableState.of( context, )!.controllers; - final style = CompletedClassStyle.resolve( + final activeStyle = CompletedClassStyle.resolve( palette: color, - isCompleted: isCompleted, + isCompleted: false, + ); + final completedStyle = CompletedClassStyle.resolve( + palette: color, + isCompleted: true, ); /// This is the result of the class info card. + const borderRadius = BorderRadius.all(Radius.circular(8)); return Padding( padding: const EdgeInsets.all(1), child: ClipRRect( - // Out - borderRadius: BorderRadius.circular(8), - child: Container( - // Border - color: style.borderColor.withValues(alpha: style.borderAlpha), - padding: const EdgeInsets.all(2), - child: Stack( - children: [ - ClipRRect( - // Inner - borderRadius: BorderRadius.circular(6), - child: Container( - color: style.innerColor.withValues(alpha: style.innerAlpha), - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - overlayColor: Colors.transparent, + borderRadius: borderRadius, + child: LayoutBuilder( + builder: (context, constraints) { + final splitHeight = completedHeight.clamp(0.0, constraints.maxHeight); + final isCompleted = splitHeight >= constraints.maxHeight - 0.5; + final textStyle = isCompleted ? completedStyle : activeStyle; + final borderStyle = isCompleted ? completedStyle : activeStyle; + + return Stack( + fit: StackFit.expand, + children: [ + if (splitHeight > 0) + Positioned( + top: 0, + left: 0, + right: 0, + height: splitHeight, + child: Container( + color: completedStyle.innerColor.withValues( + alpha: completedStyle.innerAlpha, + ), + ), + ), + if (splitHeight < constraints.maxHeight) + Positioned( + top: splitHeight, + left: 0, + right: 0, + bottom: 0, + child: Container( + color: activeStyle.innerColor.withValues( + alpha: activeStyle.innerAlpha, + ), ), - onPressed: () async { - var controller = ClassTableState.of(context)!.controllers; + ), + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: Colors.transparent, + ), + onPressed: () async { + var controller = ClassTableState.of(context)!.controllers; /// The way to show the class info of the period. /// The last one indicate whether to delete this stuff. @@ -115,54 +146,77 @@ class ClassCard extends StatelessWidget { }); } } - }, - child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Text( - name, - style: TextStyle(color: style.textColor, fontSize: isPhone(context) ? 12 : 14), - maxLines: 3, - overflow: TextOverflow.clip, + }, + child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + name, + style: TextStyle( + color: textStyle.textColor, + fontSize: isPhone(context) ? 12 : 14, ), + maxLines: 3, + overflow: TextOverflow.clip, ), - Text( - "@${place ?? FlutterI18n.translate(context, "classtable.class_card.unknown_classroom")}", - style: TextStyle(color: style.textColor, fontSize: isPhone(context) ? 10 : 12), + ), + Text( + "@${place ?? FlutterI18n.translate(context, "classtable.class_card.unknown_classroom")}", + style: TextStyle( + color: textStyle.textColor, + fontSize: isPhone(context) ? 10 : 12, ), - if (data.length > 1) - Text( - FlutterI18n.translate( - context, - "classtable.class_card.remains_hint", - translationParams: { - "remain_count": (data.length - 1) - .toString(), - }, - ), - style: TextStyle(color: style.textColor, fontSize: isPhone(context) ? 10 : 12), + ), + if (data.length > 1) + Text( + FlutterI18n.translate( + context, + "classtable.class_card.remains_hint", + translationParams: { + "remain_count": + (data.length - 1).toString(), + }, ), - ], - ) - .alignment(Alignment.topLeft) - .padding( - horizontal: isPhone(context) ? 2 : 4, - vertical: 4, - ), + style: TextStyle( + color: textStyle.textColor, + fontSize: isPhone(context) ? 10 : 12, + ), + ), + ], + ) + .alignment(Alignment.topLeft) + .padding( + horizontal: isPhone(context) ? 2 : 4, + vertical: 4, + ), + ), + if (data.length > 1) + ClipPath( + clipper: Triangle(), + child: Container( + color: textStyle.borderColor, + ).constrained(width: 8, height: 8), + ).alignment(Alignment.topRight), + Positioned.fill( + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + border: Border.all( + color: borderStyle.borderColor.withValues( + alpha: borderStyle.borderAlpha, + ), + width: 2, + ), + ), + ), ), ), - ), - if (data.length > 1) - ClipPath( - clipper: Triangle(), - child: Container( - color: style.borderColor, - ).constrained(width: 8, height: 8), - ).alignment(Alignment.topRight), - ], - ), + ], + ); + }, ), ), ); 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 bc4ad6e7..b0e35fed 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'; @@ -33,24 +34,6 @@ class ClassOrgainzedData { final MaterialColor color; - /// Following is the begin/end for each blocks... - static const _timeInBlock = [ - "08:30", - "09:20", - "10:25", - "11:15", - "12:00", - "14:00", - "14:50", - "15:55", - "16:45", - "17:30", - "19:00", - "19:55", - "20:35", - "21:25", - ]; - factory ClassOrgainzedData.fromTimeArrangement( TimeArrangement timeArrangement, MaterialColor color, @@ -127,40 +110,61 @@ class ClassOrgainzedData { }); static double _transferIndex(DateTime time) { - int timeInMin = time.hour * 60 + time.minute; - int previous = 0; - // Start from the second element. - for (var i in _timeInBlock) { - int timeChosen = - int.parse(i.split(":")[0]) * 60 + int.parse(i.split(":")[1]); - if (previous == 0) { - // Some exam is started before 8:30 - if (timeInMin < timeChosen) { - return 0; + final timeInMin = time.hour * 60 + time.minute; + if (timeList.isEmpty) { + return 0; + } + + int parseMinute(String hhmm) { + final parts = hhmm.split(':'); + return int.parse(parts[0]) * 60 + int.parse(parts[1]); + } + + double classStartBlock(int classIndex) { + if (classIndex < 4) { + return classIndex * 5.0; + } + if (classIndex < 8) { + return 23 + (classIndex - 4) * 5.0; + } + return 46 + (classIndex - 8) * 5.0; + } + + final firstStart = parseMinute(timeList.first); + if (timeInMin < firstStart) { + return 0; + } + + final classCount = timeList.length ~/ 2; + for (var classIndex = 0; classIndex < classCount; classIndex++) { + final startMinute = parseMinute(timeList[classIndex * 2]); + final endMinute = parseMinute(timeList[classIndex * 2 + 1]); + final startBlock = classStartBlock(classIndex); + final endBlock = startBlock + 5; + + if (timeInMin >= startMinute && timeInMin < endMinute) { + final ratio = (timeInMin - startMinute) / (endMinute - startMinute); + return startBlock + 5 * ratio; + } + + if (classIndex == classCount - 1) { + if (timeInMin >= endMinute) { + return 61; } - previous = timeChosen; continue; } - if (timeInMin >= previous && timeInMin < timeChosen) { - double basic = 0; - double blocks = 5; - double ratio = (timeInMin - previous) / (timeChosen - previous); - if (previous < 12 * 60) { - basic = (_timeInBlock.indexOf(i) - 1) * 5; - } else if (previous < 14 * 60) { - basic = 20; - blocks = 3; - } else if (previous < 17.5 * 60) { - basic = 23 + (_timeInBlock.indexOf(i) - 6) * 5; - } else if (previous < 19 * 60) { - basic = 43; - blocks = 3; - } else { - basic = 46 + (_timeInBlock.indexOf(i) - 11) * 5; + + final nextStartMinute = parseMinute(timeList[(classIndex + 1) * 2]); + if (timeInMin >= endMinute && timeInMin < nextStartMinute) { + final nextStartBlock = classStartBlock(classIndex + 1); + final breakMinuteSpan = nextStartMinute - endMinute; + final breakBlockSpan = nextStartBlock - endBlock; + + if (breakMinuteSpan > 0 && breakBlockSpan > 0) { + final ratio = (timeInMin - endMinute) / breakMinuteSpan; + return endBlock + breakBlockSpan * ratio; } - return basic + blocks * ratio; - } else { - previous = timeChosen; + return endBlock; } } 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 0560e0b6..ff6eee42 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -8,7 +8,6 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:watermeter/model/time_list.dart'; import 'package:watermeter/page/classtable/class_table_view/class_card.dart'; -import 'package:watermeter/page/classtable/class_table_view/completed_class_style.dart'; import 'package:watermeter/page/classtable/class_table_view/class_organized_data.dart'; import 'package:watermeter/page/classtable/class_table_view/classtable_date_row.dart'; import 'package:watermeter/page/classtable/class_table_view/current_time_indicator.dart'; @@ -44,19 +43,34 @@ class ClassTableView extends StatefulWidget { class _ClassTableViewState extends State { late ClassTableWidgetState classTableState; late BoxConstraints size; + ClassTableWidgetState? _attachedClassTableState; DateTime get _visibleWeekStart => classTableState.startDay .add(Duration(days: 7 * classTableState.offset)) .add(Duration(days: 7 * widget.index)); - bool _isCompleted(ClassOrgainzedData data, int dayIndex) { + double _completedHeight(ClassOrgainzedData data, int dayIndex) { final now = classTableState.currentTime; final dayStart = _visibleWeekStart.add(Duration(days: dayIndex - 1)); - return CompletedClassStyle.isCompleted( - data: data, - now: now, - dayStart: dayStart, + final today = DateTime(now.year, now.month, now.day); + + if (today.isBefore(dayStart)) { + return 0; + } + if (today.isAfter(dayStart)) { + return blockheight(data.stop - data.start); + } + + final currentIndex = CurrentTimeIndicator.transferTimeToBlockIndex(now); + if (currentIndex <= data.start) { + return 0; + } + + final completedBlocks = (currentIndex - data.start).clamp( + 0.0, + data.stop - data.start, ); + return blockheight(completedBlocks); } Positioned? _currentTimeIndicator() => CurrentTimeIndicator.build( @@ -96,14 +110,14 @@ class _ClassTableViewState extends State { /// Choice the day and render it! for (var i in arrangedEvents) { /// Generate the row. - final isCompleted = _isCompleted(i, index); + final completedHeight = _completedHeight(i, index); thisRow.add( Positioned( top: blockheight(i.start), height: blockheight(i.stop - i.start), left: leftRow + blockwidth * (index - 1), width: blockwidth, - child: ClassCard(detail: i, isCompleted: isCompleted), + child: ClassCard(detail: i, completedHeight: completedHeight), ), ); } @@ -213,14 +227,21 @@ class _ClassTableViewState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - classTableState = ClassTableState.of(context)!.controllers; - classTableState.addListener(_reload); + final nextClassTableState = ClassTableState.of(context)!.controllers; + + if (_attachedClassTableState != nextClassTableState) { + _attachedClassTableState?.removeListener(_reload); + _attachedClassTableState = nextClassTableState; + _attachedClassTableState!.addListener(_reload); + } + + classTableState = nextClassTableState; updateSize(); } @override void dispose() { - classTableState.removeListener(_reload); + _attachedClassTableState?.removeListener(_reload); super.dispose(); } diff --git a/lib/page/classtable/class_table_view/current_time_indicator.dart b/lib/page/classtable/class_table_view/current_time_indicator.dart index 9a76d209..c23626d2 100644 --- a/lib/page/classtable/class_table_view/current_time_indicator.dart +++ b/lib/page/classtable/class_table_view/current_time_indicator.dart @@ -2,11 +2,12 @@ // SPDX-License-Identifier: MPL-2.0 OR Apache-2.0 import 'package:flutter/material.dart'; +import 'package:watermeter/model/time_list.dart'; class CurrentTimeIndicatorConfig { static bool enabled = true; static bool showTimeLabel = true; - static bool showCurrentDayColumnBox = true; + static bool showTodayColumnHighlight = true; static double lineAlpha = 0.9; static double lineThickness = 2; static double labelHeight = 14; @@ -16,62 +17,72 @@ class CurrentTimeIndicatorConfig { } class CurrentTimeIndicator { - static const List _timeInBlock = [ - '08:30', - '09:20', - '10:25', - '11:15', - '12:00', - '14:00', - '14:50', - '15:55', - '16:45', - '17:30', - '19:00', - '19:55', - '20:35', - '21:25', - ]; - static double _transferIndex(DateTime time) { final timeInMin = time.hour * 60 + time.minute; - var previous = 0; - for (final i in _timeInBlock) { - final timeChosen = - int.parse(i.split(':')[0]) * 60 + int.parse(i.split(':')[1]); - if (previous == 0) { - if (timeInMin < timeChosen) { - return 0; + if (timeList.isEmpty) { + return 0; + } + + int parseMinute(String hhmm) { + final parts = hhmm.split(':'); + return int.parse(parts[0]) * 60 + int.parse(parts[1]); + } + + double classStartBlock(int classIndex) { + if (classIndex < 4) { + return classIndex * 5.0; + } + if (classIndex < 8) { + return 23 + (classIndex - 4) * 5.0; + } + return 46 + (classIndex - 8) * 5.0; + } + + final firstStart = parseMinute(timeList.first); + if (timeInMin < firstStart) { + return 0; + } + + final classCount = timeList.length ~/ 2; + for (var classIndex = 0; classIndex < classCount; classIndex++) { + final startMinute = parseMinute(timeList[classIndex * 2]); + final endMinute = parseMinute(timeList[classIndex * 2 + 1]); + final startBlock = classStartBlock(classIndex); + final endBlock = startBlock + 5; + + if (timeInMin >= startMinute && timeInMin < endMinute) { + final ratio = (timeInMin - startMinute) / (endMinute - startMinute); + return startBlock + 5 * ratio; + } + + if (classIndex == classCount - 1) { + if (timeInMin >= endMinute) { + return 61; } - previous = timeChosen; continue; } - if (timeInMin >= previous && timeInMin < timeChosen) { - var basic = 0.0; - var blocks = 5.0; - final ratio = (timeInMin - previous) / (timeChosen - previous); - if (previous < 12 * 60) { - basic = (_timeInBlock.indexOf(i) - 1) * 5; - } else if (previous < 14 * 60) { - basic = 20; - blocks = 3; - } else if (previous < 17.5 * 60) { - basic = 23 + (_timeInBlock.indexOf(i) - 6) * 5; - } else if (previous < 19 * 60) { - basic = 43; - blocks = 3; - } else { - basic = 46 + (_timeInBlock.indexOf(i) - 11) * 5; + final nextStartMinute = parseMinute(timeList[(classIndex + 1) * 2]); + if (timeInMin >= endMinute && timeInMin < nextStartMinute) { + final nextStartBlock = classStartBlock(classIndex + 1); + final breakMinuteSpan = nextStartMinute - endMinute; + final breakBlockSpan = nextStartBlock - endBlock; + + // Move continuously during breaks that have visible rows (e.g. lunch/dinner). + // If there is no visual gap between classes, keep the indicator at the boundary. + if (breakMinuteSpan > 0 && breakBlockSpan > 0) { + final ratio = (timeInMin - endMinute) / breakMinuteSpan; + return endBlock + breakBlockSpan * ratio; } - return basic + blocks * ratio; + return endBlock; } - previous = timeChosen; } return 61; } + static double transferTimeToBlockIndex(DateTime time) => _transferIndex(time); + static Positioned? build({ required BuildContext context, required DateTime now, @@ -97,9 +108,10 @@ class CurrentTimeIndicator { final lineTop = blockHeight(_transferIndex(now)); final color = Theme.of(context).colorScheme.primary; - final lineHorizontalInset = CurrentTimeIndicatorConfig.showCurrentDayColumnBox - ? CurrentTimeIndicatorConfig.dayColumnBorderWidth - : 0.0; + final lineHorizontalInset = + CurrentTimeIndicatorConfig.showTodayColumnHighlight + ? CurrentTimeIndicatorConfig.dayColumnBorderWidth + : 0.0; final labelText = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; final hasLabel = CurrentTimeIndicatorConfig.showTimeLabel; @@ -129,7 +141,7 @@ class CurrentTimeIndicator { ), ), Positioned( - top: lineOffset, + top: lineOffset - CurrentTimeIndicatorConfig.lineThickness / 2, left: lineHorizontalInset / 2, right: lineHorizontalInset / 2, child: Container( @@ -154,7 +166,7 @@ class CurrentTimeIndicator { required double blockWidth, required double Function(double) blockHeight, }) { - if (!CurrentTimeIndicatorConfig.showCurrentDayColumnBox) { + if (!CurrentTimeIndicatorConfig.showTodayColumnHighlight) { return null; } @@ -174,7 +186,9 @@ class CurrentTimeIndicator { ); return Positioned( - left: leftRow + blockWidth * dayOffset - + left: + leftRow + + blockWidth * dayOffset - CurrentTimeIndicatorConfig.dayColumnBorderWidth / 2, top: 0, width: blockWidth + CurrentTimeIndicatorConfig.dayColumnBorderWidth, From d8da6fda1f8ab3ce11e037f40d2ace33795b4cae Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Fri, 17 Apr 2026 17:49:23 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=91=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E8=A7=86=E5=9B=BE=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E7=8A=B6=E6=80=81=E6=8C=87=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B7=B2=E5=AE=8C=E6=88=90=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E7=9A=84=E6=A0=B7=E5=BC=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../class_page/week_choice_view.dart | 82 +++++++++++++++---- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/lib/page/classtable/class_page/week_choice_view.dart b/lib/page/classtable/class_page/week_choice_view.dart index d7eab31c..94eb0ec5 100644 --- a/lib/page/classtable/class_page/week_choice_view.dart +++ b/lib/page/classtable/class_page/week_choice_view.dart @@ -6,6 +6,8 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:watermeter/page/classtable/class_table_view/class_organized_data.dart'; +import 'package:watermeter/page/classtable/class_table_view/completed_class_style.dart'; +import 'package:watermeter/page/classtable/class_table_view/current_time_indicator.dart'; import 'package:watermeter/page/classtable/classtable_constant.dart'; import 'package:watermeter/page/classtable/classtable_state.dart'; @@ -22,6 +24,14 @@ class _WeekChoiceViewState extends State { late ClassTableWidgetState controller; // 缓存 AutoSizeGroup,避免每次 build 创建新实例 final AutoSizeGroup _autoSizeGroup = AutoSizeGroup(); + static const double _occupiedOpacity = 1.0; + static const double _completedOpacity = 0.45; + static const double _vacantOpacity = 0.25; + + Color _desaturateColor(Color color, {required double factor}) { + final hsl = HSLColor.fromColor(color); + return hsl.withSaturation(hsl.saturation * factor).toColor(); + } @override void didChangeDependencies() { @@ -31,11 +41,23 @@ class _WeekChoiceViewState extends State { /// The dot of the overview, [isOccupied] is used to identify the opacity of the dot. /// [primaryColor] is passed in from outside to avoid calling Theme.of(context) for each dot. - Widget dot({required bool isOccupied, required Color primaryColor}) { - double opacity = isOccupied ? 1 : 0.25; - return ClipOval( - child: ColoredBox(color: primaryColor.withValues(alpha: opacity)), - ); + Widget dot({ + required bool isOccupied, + required bool isCompleted, + required Color primaryColor, + }) { + double opacity = _vacantOpacity; + Color dotColor = primaryColor; + if (isOccupied) { + opacity = isCompleted ? _completedOpacity : _occupiedOpacity; + if (isCompleted) { + dotColor = _desaturateColor( + primaryColor, + factor: CompletedClassStyleConfig.completedSaturationFactor, + ); + } + } + return ClipOval(child: ColoredBox(color: dotColor.withValues(alpha: opacity))); } /// [buttonInformaion] shows the botton's [index] and the overview. @@ -80,10 +102,19 @@ class _WeekChoiceViewState extends State { int day = i % 5 + 1; int time = i ~/ 5; bool isOccupied = false; + bool isCompleted = false; + final now = controller.currentTime; + final today = DateTime(now.year, now.month, now.day); + final weekStart = controller.startDay + .add(Duration(days: 7 * controller.offset)) + .add(Duration(days: 7 * widget.index)); + final blockDate = weekStart.add(Duration(days: day - 1)); + final currentBlockIndex = CurrentTimeIndicator + .transferTimeToBlockIndex(now); List arrangedEvents = controller .getArrangement(weekIndex: widget.index, dayIndex: day); - for (var i in arrangedEvents) { + for (var event in arrangedEvents) { int start = 0; int stop = 0; @@ -95,17 +126,20 @@ class _WeekChoiceViewState extends State { case 1: start = 10; stop = 20; - isOccupied = i.stop > 10.0 && i.stop <= 20.0; + isOccupied = + event.stop > 10.0 && event.stop <= 20.0; break; case 2: start = 20; stop = 33; - isOccupied = i.stop > 23.0 && i.stop <= 33.0; + isOccupied = + event.stop > 23.0 && event.stop <= 33.0; break; case 3: start = 33; stop = 43; - isOccupied = i.stop > 33.0 && i.stop <= 43.0; + isOccupied = + event.stop > 33.0 && event.stop <= 43.0; break; case 4: start = 46; @@ -113,16 +147,36 @@ class _WeekChoiceViewState extends State { break; } - if ((i.stop != start && i.start != stop) && - ((start < i.stop && i.start < stop) || - (stop > i.start && i.stop > start))) { + if ((event.stop != start && event.start != stop) && + ((start < event.stop && event.start < stop) || + (stop > event.start && event.stop > start))) { isOccupied = true; } - if (isOccupied) break; + if (!isOccupied) { + continue; + } + + if (blockDate.isBefore(today)) { + isCompleted = true; + } else if (blockDate.isAfter(today)) { + isCompleted = false; + } else { + isCompleted = stop <= currentBlockIndex; + } + + // Any ongoing/upcoming arrangement in the same cell should keep + // the preview block highlighted as active. + if (!isCompleted) { + break; + } } - return dot(isOccupied: isOccupied, primaryColor: primaryColor); + return dot( + isOccupied: isOccupied, + isCompleted: isCompleted, + primaryColor: primaryColor, + ); }), ), ), From 5174d6cc07bb7a7cbe7650fd1e1951b29adac9a4 Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Fri, 17 Apr 2026 19:08:37 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E5=9C=A8=E5=90=84=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=99=A8=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AF=BE=E8=A1=A8=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/controller/classtable_controller.dart | 3 +++ lib/controller/exam_controller.dart | 3 +++ lib/controller/other_experiment_controller.dart | 3 +++ lib/controller/physics_experiment_controller.dart | 3 +++ lib/page/classtable/classtable_state.dart | 8 ++++---- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/controller/classtable_controller.dart b/lib/controller/classtable_controller.dart index cebfac08..e9c6bdfa 100644 --- a/lib/controller/classtable_controller.dart +++ b/lib/controller/classtable_controller.dart @@ -143,6 +143,9 @@ class ClassTableController { Future reloadClassTable() async { if (schoolClassTableSignal.value.isLoading) return; + if (schoolClassTableSignal.value is AsyncError) { + schoolClassTableSignal.reset(); + } await schoolClassTableSignal.reload().catchError( (e, s) => log.handle( e, diff --git a/lib/controller/exam_controller.dart b/lib/controller/exam_controller.dart index 85b2d8b4..7e1ef49b 100644 --- a/lib/controller/exam_controller.dart +++ b/lib/controller/exam_controller.dart @@ -63,6 +63,9 @@ class ExamController { Future reloadExamInfo() async { if (examInfoSignal.value.isLoading) return; + if (examInfoSignal.value is AsyncError) { + examInfoSignal.reset(); + } return await examInfoSignal.reload().catchError( (e, s) => log.handle(e, s, "[ExamController][reloadExamInfo] Have issue"), ); diff --git a/lib/controller/other_experiment_controller.dart b/lib/controller/other_experiment_controller.dart index b6e875d4..bfbd3ce4 100644 --- a/lib/controller/other_experiment_controller.dart +++ b/lib/controller/other_experiment_controller.dart @@ -63,6 +63,9 @@ class OtherExperimentController { Future reloadOtherExperiment() async { if (otherExperimentSignal.value.isLoading) return; + if (otherExperimentSignal.value is AsyncError) { + otherExperimentSignal.reset(); + } await otherExperimentSignal.reload().catchError( (e, s) => log.handle( e, diff --git a/lib/controller/physics_experiment_controller.dart b/lib/controller/physics_experiment_controller.dart index 5e783114..a1a69866 100644 --- a/lib/controller/physics_experiment_controller.dart +++ b/lib/controller/physics_experiment_controller.dart @@ -71,6 +71,9 @@ class PhysicsExperimentController { Future reloadPhysicsExperiment() async { if (physicsExperimentSignal.value.isLoading) return; + if (physicsExperimentSignal.value is AsyncError) { + physicsExperimentSignal.reset(); + } await physicsExperimentSignal.reload().catchError( (e, s) => log.handle( e, diff --git a/lib/page/classtable/classtable_state.dart b/lib/page/classtable/classtable_state.dart index 2e4c366f..37a607cc 100644 --- a/lib/page/classtable/classtable_state.dart +++ b/lib/page/classtable/classtable_state.dart @@ -579,10 +579,10 @@ END:VTIMEZONE Future updateClasstable(BuildContext context) async { log.info("Updating time arrangement data..."); return await Future.wait([ - classTableController.schoolClassTableSignal.reload(), - examController.examInfoSignal.reload(), - physicsExperimentController.physicsExperimentSignal.reload(), - otherExperimentController.otherExperimentSignal.reload(), + classTableController.reloadClassTable(), + examController.reloadExamInfo(), + physicsExperimentController.reloadPhysicsExperiment(), + otherExperimentController.reloadOtherExperiment(), ]).then((value) { notifyListeners(); }); From 39c25dc5be8edc788be38994e64d7c36a84a22ee Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Fri, 17 Apr 2026 19:53:26 +0800 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E6=97=B6=E9=97=B4=E6=8C=87=E7=A4=BA=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=92=8C=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/flutter_i18n/en_US.yaml | 2 +- assets/flutter_i18n/zh_CN.yaml | 4 +- assets/flutter_i18n/zh_TW.yaml | 4 +- .../class_page/week_choice_view.dart | 121 ++++++++++-------- .../completed_class_style.dart | 12 +- .../current_time_indicator.dart | 59 +++++++-- 6 files changed, 126 insertions(+), 76 deletions(-) diff --git a/assets/flutter_i18n/en_US.yaml b/assets/flutter_i18n/en_US.yaml index 86f801d1..847da77f 100644 --- a/assets/flutter_i18n/en_US.yaml +++ b/assets/flutter_i18n/en_US.yaml @@ -166,7 +166,7 @@ classtable: show_current_time_indicator: "Show current time indicator" show_current_time_label: "Show mini time label" show_today_column_highlight: "Highlight today's column" - unfinished_section: "Unfinished class style" + unfinished_section: "Unstarted class style" active_border_alpha: "Border opacity: {value}" active_inner_alpha: "Fill opacity: {value}" completed_section: "Completed class style" diff --git a/assets/flutter_i18n/zh_CN.yaml b/assets/flutter_i18n/zh_CN.yaml index e7f663f8..6e7121b2 100644 --- a/assets/flutter_i18n/zh_CN.yaml +++ b/assets/flutter_i18n/zh_CN.yaml @@ -161,10 +161,10 @@ classtable: show_current_time_indicator: "显示当前时间指示线" show_current_time_label: "显示迷你数字时钟" show_today_column_highlight: "强调显示今天的纵列" - unfinished_section: "未完成课程样式" + unfinished_section: "未开始课程样式" active_border_alpha: "边框透明度: {value}" active_inner_alpha: "底色透明度: {value}" - completed_section: "已完成课程样式" + completed_section: "已结束课程样式" completed_saturation_factor: "底色饱和度: {value}" completed_text_saturation_factor: "文字饱和度: {value}" completed_border_alpha: "边框透明度: {value}" diff --git a/assets/flutter_i18n/zh_TW.yaml b/assets/flutter_i18n/zh_TW.yaml index 47320f3d..e0404bd3 100644 --- a/assets/flutter_i18n/zh_TW.yaml +++ b/assets/flutter_i18n/zh_TW.yaml @@ -137,10 +137,10 @@ classtable: show_current_time_indicator: 顯示當前時間指示線 show_current_time_label: 顯示迷你数字時钟 show_today_column_highlight: 强调顯示今天的纵列 - unfinished_section: 未完成課程樣式 + unfinished_section: 未开始課程樣式 active_border_alpha: 邊框透明度: {value} active_inner_alpha: 底色透明度: {value} - completed_section: 已完成課程樣式 + completed_section: 已结束課程樣式 completed_saturation_factor: 底色飽和度: {value} completed_text_saturation_factor: 文字飽和度: {value} completed_border_alpha: 邊框透明度: {value} diff --git a/lib/page/classtable/class_page/week_choice_view.dart b/lib/page/classtable/class_page/week_choice_view.dart index 94eb0ec5..5a32e254 100644 --- a/lib/page/classtable/class_page/week_choice_view.dart +++ b/lib/page/classtable/class_page/week_choice_view.dart @@ -33,6 +33,47 @@ class _WeekChoiceViewState extends State { return hsl.withSaturation(hsl.saturation * factor).toColor(); } + ({int start, int stop}) _slotRange(int timeIndex) { + switch (timeIndex) { + case 0: + return (start: 0, stop: 10); + case 1: + return (start: 10, stop: 20); + case 2: + return (start: 20, stop: 33); + case 3: + return (start: 33, stop: 43); + default: + // 49 is the visible limit of the compact week preview. + return (start: 46, stop: 49); + } + } + + bool _eventOccupiesSlot({ + required ClassOrgainzedData event, + required int slotStart, + required int slotStop, + }) { + return (event.stop != slotStart && event.start != slotStop) && + ((slotStart < event.stop && event.start < slotStop) || + (slotStop > event.start && event.stop > slotStart)); + } + + bool _slotCompleted({ + required DateTime slotDate, + required DateTime today, + required int slotStop, + required double currentBlockIndex, + }) { + if (slotDate.isBefore(today)) { + return true; + } + if (slotDate.isAfter(today)) { + return false; + } + return slotStop <= currentBlockIndex; + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -101,80 +142,54 @@ class _WeekChoiceViewState extends State { children: List.generate(25, (i) { int day = i % 5 + 1; int time = i ~/ 5; - bool isOccupied = false; - bool isCompleted = false; final now = controller.currentTime; final today = DateTime(now.year, now.month, now.day); final weekStart = controller.startDay - .add(Duration(days: 7 * controller.offset)) - .add(Duration(days: 7 * widget.index)); + .add(Duration(days: 7 * controller.offset)) + .add(Duration(days: 7 * widget.index)); final blockDate = weekStart.add(Duration(days: day - 1)); final currentBlockIndex = CurrentTimeIndicator - .transferTimeToBlockIndex(now); + .transferTimeToBlockIndex(now); List arrangedEvents = controller .getArrangement(weekIndex: widget.index, dayIndex: day); - for (var event in arrangedEvents) { - int start = 0; - int stop = 0; - - switch (time) { - case 0: - start = 0; - stop = 10; - break; - case 1: - start = 10; - stop = 20; - isOccupied = - event.stop > 10.0 && event.stop <= 20.0; - break; - case 2: - start = 20; - stop = 33; - isOccupied = - event.stop > 23.0 && event.stop <= 33.0; - break; - case 3: - start = 33; - stop = 43; - isOccupied = - event.stop > 33.0 && event.stop <= 43.0; - break; - case 4: - start = 46; - stop = 49; // 49 is the limit of the classtable... - break; - } + final slot = _slotRange(time); - if ((event.stop != start && event.start != stop) && - ((start < event.stop && event.start < stop) || - (stop > event.start && event.stop > start))) { - isOccupied = true; - } + var hasOccupiedEvent = false; + var allOccupiedEventsCompleted = true; - if (!isOccupied) { + for (var event in arrangedEvents) { + final eventOccupiesCell = _eventOccupiesSlot( + event: event, + slotStart: slot.start, + slotStop: slot.stop, + ); + if (!eventOccupiesCell) { continue; } - if (blockDate.isBefore(today)) { - isCompleted = true; - } else if (blockDate.isAfter(today)) { - isCompleted = false; - } else { - isCompleted = stop <= currentBlockIndex; - } + hasOccupiedEvent = true; + + final eventCompleted = _slotCompleted( + slotDate: blockDate, + today: today, + slotStop: slot.stop, + currentBlockIndex: currentBlockIndex, + ); + + allOccupiedEventsCompleted = + allOccupiedEventsCompleted && eventCompleted; // Any ongoing/upcoming arrangement in the same cell should keep // the preview block highlighted as active. - if (!isCompleted) { + if (!eventCompleted) { break; } } return dot( - isOccupied: isOccupied, - isCompleted: isCompleted, + isOccupied: hasOccupiedEvent, + isCompleted: hasOccupiedEvent && allOccupiedEventsCompleted, primaryColor: primaryColor, ); }), diff --git a/lib/page/classtable/class_table_view/completed_class_style.dart b/lib/page/classtable/class_table_view/completed_class_style.dart index 3813272d..38138bf4 100644 --- a/lib/page/classtable/class_table_view/completed_class_style.dart +++ b/lib/page/classtable/class_table_view/completed_class_style.dart @@ -9,14 +9,14 @@ import 'package:watermeter/page/classtable/class_table_view/class_organized_data class CompletedClassStyleConfig { /// Completed-card color tuning. /// Lower values make finished classes look more muted and faded. - static double completedSaturationFactor = 0.35; - static double completedTextSaturationFactor = 0.55; - static double completedBorderAlpha = 0.55; - static double completedInnerAlpha = 0.45; + static double completedSaturationFactor = 0.5; + static double completedTextSaturationFactor = 0.75; + static double completedBorderAlpha = 0.75; + static double completedInnerAlpha = 0.5; /// Active-card baseline appearance. - static double activeBorderAlpha = 0.8; - static double activeInnerAlpha = 0.7; + static double activeBorderAlpha = 1.0; + static double activeInnerAlpha = 0.9; } class CompletedClassStyleData { diff --git a/lib/page/classtable/class_table_view/current_time_indicator.dart b/lib/page/classtable/class_table_view/current_time_indicator.dart index c23626d2..80faa6f7 100644 --- a/lib/page/classtable/class_table_view/current_time_indicator.dart +++ b/lib/page/classtable/class_table_view/current_time_indicator.dart @@ -6,12 +6,16 @@ import 'package:watermeter/model/time_list.dart'; class CurrentTimeIndicatorConfig { static bool enabled = true; - static bool showTimeLabel = true; + static bool showTimeLabel = false; static bool showTodayColumnHighlight = true; static double lineAlpha = 0.9; static double lineThickness = 2; static double labelHeight = 14; static double labelFontSize = 9; + static double labelBackgroundAlpha = 0.45; + static double labelHorizontalPadding = 2; + static double labelVerticalPadding = 0; + static double labelBorderRadius = 4; static double dayColumnBorderAlpha = 0.65; static double dayColumnBorderWidth = 2; } @@ -107,7 +111,8 @@ class CurrentTimeIndicator { } final lineTop = blockHeight(_transferIndex(now)); - final color = Theme.of(context).colorScheme.primary; + final colorScheme = Theme.of(context).colorScheme; + final color = colorScheme.primary; final lineHorizontalInset = CurrentTimeIndicatorConfig.showTodayColumnHighlight ? CurrentTimeIndicatorConfig.dayColumnBorderWidth @@ -115,13 +120,16 @@ class CurrentTimeIndicator { final labelText = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; final hasLabel = CurrentTimeIndicatorConfig.showTimeLabel; - final labelHeight = hasLabel ? CurrentTimeIndicatorConfig.labelHeight : 0.0; - final labelTop = lineTop > labelHeight ? lineTop - labelHeight : 0.0; - final lineOffset = lineTop - labelTop; + final labelHeight = CurrentTimeIndicatorConfig.labelHeight; + final indicatorTop = lineTop > labelHeight ? lineTop - labelHeight : 0.0; + final lineOffset = lineTop - indicatorTop; + final labelBackgroundColor = colorScheme.surface.withValues( + alpha: CurrentTimeIndicatorConfig.labelBackgroundAlpha, + ); return Positioned( left: leftRow + blockWidth * dayOffset, - top: labelTop, + top: indicatorTop, width: blockWidth, child: IgnorePointer( child: SizedBox( @@ -131,12 +139,39 @@ class CurrentTimeIndicator { if (hasLabel) Align( alignment: Alignment.topCenter, - child: Text( - labelText, - style: TextStyle( - fontSize: CurrentTimeIndicatorConfig.labelFontSize, - color: color, - fontWeight: FontWeight.w600, + child: DecoratedBox( + decoration: BoxDecoration( + color: labelBackgroundColor, + border: Border.all( + color: color.withValues(alpha: 0.2), + width: 0.6, + ), + borderRadius: BorderRadius.circular( + CurrentTimeIndicatorConfig.labelBorderRadius, + ), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: + CurrentTimeIndicatorConfig.labelHorizontalPadding, + vertical: + CurrentTimeIndicatorConfig.labelVerticalPadding, + ), + child: Text( + labelText, + style: TextStyle( + fontSize: CurrentTimeIndicatorConfig.labelFontSize, + color: color, + fontWeight: FontWeight.w700, + shadows: const [ + Shadow( + offset: Offset(0, 0), + blurRadius: 2, + color: Colors.black26, + ), + ], + ), + ), ), ), ), From ed6f3c438819d34eeaacc54ab83362df0753f62c Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Fri, 17 Apr 2026 20:11:46 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../class_table_view/class_card.dart | 202 +++++++++--------- 1 file changed, 104 insertions(+), 98 deletions(-) diff --git a/lib/page/classtable/class_table_view/class_card.dart b/lib/page/classtable/class_table_view/class_card.dart index 3a9e494d..be3d28a6 100644 --- a/lib/page/classtable/class_table_view/class_card.dart +++ b/lib/page/classtable/class_table_view/class_card.dart @@ -16,7 +16,7 @@ import 'package:watermeter/page/classtable/classtable_state.dart'; import 'package:watermeter/page/public_widget/both_side_sheet.dart'; import 'package:watermeter/page/public_widget/public_widget.dart'; -/// The card in [classSubRow], metioned in [ClassTableView]. +/// The card in [classSubRow], mentioned in [ClassTableView]. class ClassCard extends StatelessWidget { final ClassOrgainzedData detail; final double completedHeight; @@ -33,9 +33,7 @@ class ClassCard extends StatelessWidget { @override Widget build(BuildContext context) { - ClassTableWidgetState classTableState = ClassTableState.of( - context, - )!.controllers; + final classTableState = ClassTableState.of(context)!.controllers; final activeStyle = CompletedClassStyle.resolve( palette: color, isCompleted: false, @@ -53,7 +51,10 @@ class ClassCard extends StatelessWidget { borderRadius: borderRadius, child: LayoutBuilder( builder: (context, constraints) { - final splitHeight = completedHeight.clamp(0.0, constraints.maxHeight); + final splitHeight = completedHeight.clamp( + 0.0, + constraints.maxHeight, + ); final isCompleted = splitHeight >= constraints.maxHeight - 0.5; final textStyle = isCompleted ? completedStyle : activeStyle; final borderStyle = isCompleted ? completedStyle : activeStyle; @@ -91,106 +92,111 @@ class ClassCard extends StatelessWidget { overlayColor: Colors.transparent, ), onPressed: () async { - var controller = ClassTableState.of(context)!.controllers; + final controller = ClassTableState.of(context)!.controllers; - /// The way to show the class info of the period. - /// The last one indicate whether to delete this stuff. - (ClassDetail, TimeArrangement, bool)? toUse = - await BothSideSheet.show( - title: FlutterI18n.translate( - context, - "classtable.class_card.title", - ), - child: ArrangementDetail( - information: List.generate(data.length, (index) { - if (data.elementAt(index) is Subject || - data.elementAt(index) is ExperimentData) { - return data.elementAt(index); - } else { - return ( - classTableState.getClassDetail( - classTableState.timeArrangement.indexOf( - data.elementAt(index), - ), - ), - data.elementAt(index), - ); - } - }), - currentWeek: classTableState.currentWeek, - ), - context: context, - ); - if (context.mounted && toUse != null) { - if (toUse.$3) { - await ClassTableState.of( + // Show the class info for this card. + // The last value indicates whether to delete it. + final toUse = + await BothSideSheet.show< + (ClassDetail, TimeArrangement, bool) + >( + title: FlutterI18n.translate( context, - )!.controllers.deleteUserDefinedClass(toUse.$2); - } else { - await Navigator.of(context) - .push( - MaterialPageRoute( - builder: (context) => ClassAddWindow( - toChange: (toUse.$1, toUse.$2), - semesterLength: controller.semesterLength, + "classtable.class_card.title", + ), + child: ArrangementDetail( + information: List.generate(data.length, (index) { + if (data.elementAt(index) is Subject || + data.elementAt(index) is ExperimentData) { + return data.elementAt(index); + } + + final arrangement = data.elementAt(index); + return ( + classTableState.getClassDetail( + classTableState.timeArrangement.indexOf( + arrangement, ), ), - ) - .then((value) { - if (value == null) return; - controller.editUserDefinedClass( - value.$1, - value.$2, - value.$3, - ); - }); - } - } - }, - child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Text( - name, - style: TextStyle( - color: textStyle.textColor, - fontSize: isPhone(context) ? 12 : 14, - ), - maxLines: 3, - overflow: TextOverflow.clip, + arrangement, + ); + }), + currentWeek: classTableState.currentWeek, + ), + context: context, + ); + if (context.mounted && toUse != null) { + if (toUse.$3) { + await ClassTableState.of( + context, + )!.controllers.deleteUserDefinedClass(toUse.$2); + } else { + await Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => ClassAddWindow( + toChange: (toUse.$1, toUse.$2), + semesterLength: controller.semesterLength, ), ), - Text( - "@${place ?? FlutterI18n.translate(context, "classtable.class_card.unknown_classroom")}", - style: TextStyle( - color: textStyle.textColor, - fontSize: isPhone(context) ? 10 : 12, - ), + ) + .then((value) { + if (value == null) return; + controller.editUserDefinedClass( + value.$1, + value.$2, + value.$3, + ); + }); + } + } + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isPhone(context) ? 2 : 4, + vertical: 4, + ), + child: Align( + alignment: Alignment.topLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + name, + style: TextStyle( + color: textStyle.textColor, + fontSize: isPhone(context) ? 12 : 14, ), - if (data.length > 1) - Text( - FlutterI18n.translate( - context, - "classtable.class_card.remains_hint", - translationParams: { - "remain_count": - (data.length - 1).toString(), - }, - ), - style: TextStyle( - color: textStyle.textColor, - fontSize: isPhone(context) ? 10 : 12, - ), - ), - ], - ) - .alignment(Alignment.topLeft) - .padding( - horizontal: isPhone(context) ? 2 : 4, - vertical: 4, + maxLines: 3, + overflow: TextOverflow.clip, + ), + ), + Text( + "@${place ?? FlutterI18n.translate(context, "classtable.class_card.unknown_classroom")}", + style: TextStyle( + color: textStyle.textColor, + fontSize: isPhone(context) ? 10 : 12, + ), ), + if (data.length > 1) + Text( + FlutterI18n.translate( + context, + "classtable.class_card.remains_hint", + translationParams: { + "remain_count": (data.length - 1).toString(), + }, + ), + style: TextStyle( + color: textStyle.textColor, + fontSize: isPhone(context) ? 10 : 12, + ), + ), + ], + ), + ), + ), ), if (data.length > 1) ClipPath( @@ -226,7 +232,7 @@ class ClassCard extends StatelessWidget { class Triangle extends CustomClipper { @override Path getClip(Size size) { - Path path = Path(); + final path = Path(); path.addPolygon([ const Offset(0, 0), Offset(size.width, 0), From 82baf223fbb1d4bf52131850e32b7e451817cf05 Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Fri, 17 Apr 2026 23:00:38 +0800 Subject: [PATCH 08/11] =?UTF-8?q?refactor(classtable):=20=E8=AF=BE?= =?UTF-8?q?=E8=A1=A8=E6=97=B6=E9=97=B4=E5=88=B7=E6=96=B0=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=A1=A5=E6=8E=A5=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=85=A8=E5=B1=80signal=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/page/classtable/classtable_state.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/page/classtable/classtable_state.dart b/lib/page/classtable/classtable_state.dart index 37a607cc..90de5881 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'; @@ -12,7 +13,6 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:watermeter/controller/classtable_controller.dart'; import 'package:watermeter/controller/exam_controller.dart'; -import 'package:watermeter/controller/global_timer_controller.dart'; import 'package:watermeter/controller/physics_experiment_controller.dart'; import 'package:watermeter/controller/other_experiment_controller.dart'; import 'package:watermeter/controller/week_swift_controller.dart'; @@ -63,10 +63,13 @@ class ClassTableWidgetState with ChangeNotifier { ///*******************************************************************/// bool _disposed = false; final List _effectCleanup = []; + Timer? _clockTimer; + DateTime _currentTime = DateTime.now(); @override void dispose() { _disposed = true; + _clockTimer?.cancel(); for (final cleanup in _effectCleanup) { cleanup(); } @@ -113,12 +116,18 @@ class ClassTableWidgetState with ChangeNotifier { otherExperimentController.isOtherExperimentFromCache.value; otherExperimentController.otherExperimentCacheHintKey.value; weekSwiftController.weekSwiftSignal.value; - GlobalTimerController.i.currentTimeSignal.value; notifyListeners(); }, debugLabel: "ClassTableWidgetStateSignalBridgeEffect"), ); } + void _initClockTimer() { + _clockTimer = Timer.periodic(const Duration(seconds: 15), (_) { + _currentTime = DateTime.now(); + notifyListeners(); + }); + } + /// The length of the semester, the amount of the class table. int get semesterLength => classTableController.classTableComputedSignal.value.semesterLength; @@ -268,7 +277,7 @@ class ClassTableWidgetState with ChangeNotifier { /// The currentWeek. final int currentWeek; - DateTime get currentTime => GlobalTimerController.i.currentTimeSignal.value; + DateTime get currentTime => _currentTime; /// The exam list. List get subjects => examController.subjects.value; @@ -590,6 +599,7 @@ END:VTIMEZONE ClassTableWidgetState({required this.currentWeek}) { _initEffects(); + _initClockTimer(); if (currentWeek < 0) { _chosenWeek = 0; } else if (currentWeek >= semesterLength) { From b0c53ea91135f9d366b9488b17931c7a83f13428 Mon Sep 17 00:00:00 2001 From: Lagrange-X <110022915+Lagrange-X@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:15:54 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20gradle-wrapper.prope?= =?UTF-8?q?rties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从-all改回-bin --- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 78a77f33..50f53e39 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Apr 16 09:43:18 CST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 94ea853a02bf3eeaecb1c8af0ad38eff67dd708c Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Sat, 18 Apr 2026 20:39:55 +0800 Subject: [PATCH 10/11] =?UTF-8?q?feat(classtable):=20=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=A2=9E=E5=8A=A0=E4=BA=AE=E5=BA=A6=E8=B0=83?= =?UTF-8?q?=E8=8A=82=EF=BC=8C=E6=94=B9=E5=8F=98=E4=BA=86=E5=BC=BA=E8=B0=83?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=A0=B7=E5=BC=8F=EF=BC=8C=E6=94=B9=E5=8F=98?= =?UTF-8?q?=E4=BA=86=E6=97=B6=E9=97=B4=E6=A0=87=E7=AD=BE=E7=9A=84=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/flutter_i18n/en_US.yaml | 8 +- assets/flutter_i18n/zh_CN.yaml | 8 +- assets/flutter_i18n/zh_TW.yaml | 10 +- .../class_page/content_classtable_page.dart | 167 ++++++++++++++---- .../class_table_view/class_table_view.dart | 9 +- .../completed_class_style.dart | 52 ++++-- .../current_time_indicator.dart | 120 ++++++++----- 7 files changed, 271 insertions(+), 103 deletions(-) diff --git a/assets/flutter_i18n/en_US.yaml b/assets/flutter_i18n/en_US.yaml index 847da77f..690ec3e0 100644 --- a/assets/flutter_i18n/en_US.yaml +++ b/assets/flutter_i18n/en_US.yaml @@ -159,18 +159,22 @@ classtable: output_to_system: "Export to system calendar" refresh_classtable: "Refresh schedule" switch_semester: "Switch classtable semester" - visual_settings: "Schedule appearance settings" + current_time_settings: "Time indicator settings" + class_color_settings: "Class color settings" visual_settings: - title: "Schedule appearance settings" + current_time_settings_title: "Time indicator settings" + class_color_settings_title: "Class color settings" current_time_section: "Time indicators" show_current_time_indicator: "Show current time indicator" show_current_time_label: "Show mini time label" show_today_column_highlight: "Highlight today's column" unfinished_section: "Unstarted class style" + active_brightness_factor: "Brightness: {value}" active_border_alpha: "Border opacity: {value}" active_inner_alpha: "Fill opacity: {value}" completed_section: "Completed class style" completed_saturation_factor: "Fill saturation: {value}" + completed_brightness_factor: "Brightness: {value}" completed_text_saturation_factor: "Text saturation: {value}" completed_border_alpha: "Border opacity: {value}" completed_inner_alpha: "Fill opacity: {value}" diff --git a/assets/flutter_i18n/zh_CN.yaml b/assets/flutter_i18n/zh_CN.yaml index 6e7121b2..b23467b1 100644 --- a/assets/flutter_i18n/zh_CN.yaml +++ b/assets/flutter_i18n/zh_CN.yaml @@ -154,18 +154,22 @@ classtable: output_to_system: "导出到系统日历" refresh_classtable: "刷新日程表" switch_semester: "切换课程表学期" - visual_settings: "课表外观设置" + current_time_settings: "时间指示设置" + class_color_settings: "课表样式设置" visual_settings: - title: "课表外观设置" + current_time_settings_title: "时间指示设置" + class_color_settings_title: "课表样式设置" current_time_section: "时间指示" show_current_time_indicator: "显示当前时间指示线" show_current_time_label: "显示迷你数字时钟" show_today_column_highlight: "强调显示今天的纵列" unfinished_section: "未开始课程样式" + active_brightness_factor: "亮度: {value}" active_border_alpha: "边框透明度: {value}" active_inner_alpha: "底色透明度: {value}" completed_section: "已结束课程样式" completed_saturation_factor: "底色饱和度: {value}" + completed_brightness_factor: "亮度: {value}" completed_text_saturation_factor: "文字饱和度: {value}" completed_border_alpha: "边框透明度: {value}" completed_inner_alpha: "底色透明度: {value}" diff --git a/assets/flutter_i18n/zh_TW.yaml b/assets/flutter_i18n/zh_TW.yaml index e0404bd3..109d10ea 100644 --- a/assets/flutter_i18n/zh_TW.yaml +++ b/assets/flutter_i18n/zh_TW.yaml @@ -130,18 +130,22 @@ classtable: output_to_system: 導出到系統日曆 refresh_classtable: 刷新日程表 switch_semester: 切換課程表學期 - visual_settings: 課表外觀設置 + current_time_settings: 時間指示設置 + class_color_settings: 课表样式设置 visual_settings: - title: 課表外觀設置 + current_time_settings_title: 時間指示設置 + class_color_settings_title: 课表样式设置 current_time_section: 時間指示 show_current_time_indicator: 顯示當前時間指示線 show_current_time_label: 顯示迷你数字時钟 - show_today_column_highlight: 强调顯示今天的纵列 + show_today_column_highlight: 強調顯示今天的纵列 unfinished_section: 未开始課程樣式 + active_brightness_factor: 亮度: {value} active_border_alpha: 邊框透明度: {value} active_inner_alpha: 底色透明度: {value} completed_section: 已结束課程樣式 completed_saturation_factor: 底色飽和度: {value} + completed_brightness_factor: 亮度: {value} completed_text_saturation_factor: 文字飽和度: {value} completed_border_alpha: 邊框透明度: {value} completed_inner_alpha: 底色透明度: {value} diff --git a/lib/page/classtable/class_page/content_classtable_page.dart b/lib/page/classtable/class_page/content_classtable_page.dart index 1bf0bb60..e164628a 100644 --- a/lib/page/classtable/class_page/content_classtable_page.dart +++ b/lib/page/classtable/class_page/content_classtable_page.dart @@ -262,21 +262,13 @@ class _ContentClassTablePageState extends State { ); } - Future _showClassTableVisualSettingsDialog() async { + String _formatPercent(double value) => "${(value * 100).round()}%"; + + Future _showCurrentTimeSettingsDialog() async { var enabled = CurrentTimeIndicatorConfig.enabled; var showTimeLabel = CurrentTimeIndicatorConfig.showTimeLabel; var showTodayColumnHighlight = CurrentTimeIndicatorConfig.showTodayColumnHighlight; - var activeBorderAlpha = CompletedClassStyleConfig.activeBorderAlpha; - var activeInnerAlpha = CompletedClassStyleConfig.activeInnerAlpha; - var completedSaturationFactor = - CompletedClassStyleConfig.completedSaturationFactor; - var completedTextSaturationFactor = - CompletedClassStyleConfig.completedTextSaturationFactor; - var completedBorderAlpha = CompletedClassStyleConfig.completedBorderAlpha; - var completedInnerAlpha = CompletedClassStyleConfig.completedInnerAlpha; - - String formatPercent(double value) => "${(value * 100).round()}%"; final shouldApply = await showDialog( @@ -286,7 +278,7 @@ class _ContentClassTablePageState extends State { title: Text( FlutterI18n.translate( context, - "classtable.visual_settings.title", + "classtable.visual_settings.current_time_settings_title", ), ), content: SizedBox( @@ -296,13 +288,6 @@ class _ContentClassTablePageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - FlutterI18n.translate( - context, - "classtable.visual_settings.current_time_section", - ), - style: TextStyle(fontWeight: FontWeight.w700), - ), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text( @@ -342,20 +327,103 @@ class _ContentClassTablePageState extends State { () => showTodayColumnHighlight = value, ), ), - const Divider(height: 24), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(FlutterI18n.translate(context, "cancel")), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(FlutterI18n.translate(context, "confirm")), + ), + ], + ), + ), + ) ?? + false; + + if (!shouldApply || !mounted) { + return; + } + + CurrentTimeIndicatorConfig.enabled = enabled; + CurrentTimeIndicatorConfig.showTimeLabel = showTimeLabel; + CurrentTimeIndicatorConfig.showTodayColumnHighlight = + showTodayColumnHighlight; + setState(() {}); + } + + Future _showClassColorSettingsDialog() async { + var activeBrightnessFactor = CompletedClassStyleConfig + .activeBrightnessFactor + .clamp(0.5, 1.0) + .toDouble(); + var activeBorderAlpha = CompletedClassStyleConfig.activeBorderAlpha; + var activeInnerAlpha = CompletedClassStyleConfig.activeInnerAlpha; + var completedSaturationFactor = + CompletedClassStyleConfig.completedSaturationFactor; + var completedBrightnessFactor = CompletedClassStyleConfig + .completedBrightnessFactor + .clamp(0.5, 1.0) + .toDouble(); + var completedTextSaturationFactor = + CompletedClassStyleConfig.completedTextSaturationFactor; + var completedBorderAlpha = CompletedClassStyleConfig.completedBorderAlpha; + var completedInnerAlpha = CompletedClassStyleConfig.completedInnerAlpha; + + final shouldApply = + await showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.class_color_settings_title", + ), + ), + content: SizedBox( + width: 420, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( FlutterI18n.translate( context, "classtable.visual_settings.unfinished_section", ), - style: TextStyle(fontWeight: FontWeight.w700), + style: const TextStyle(fontWeight: FontWeight.w700), + ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.active_brightness_factor", + translationParams: { + "value": _formatPercent(activeBrightnessFactor), + }, + ), + ), + Slider( + value: activeBrightnessFactor, + min: 0.5, + max: 1.0, + divisions: 10, + onChanged: (value) => setDialogState( + () => activeBrightnessFactor = value, + ), ), Text( FlutterI18n.translate( context, "classtable.visual_settings.active_border_alpha", translationParams: { - "value": formatPercent(activeBorderAlpha), + "value": _formatPercent(activeBorderAlpha), }, ), ), @@ -372,7 +440,7 @@ class _ContentClassTablePageState extends State { context, "classtable.visual_settings.active_inner_alpha", translationParams: { - "value": formatPercent(activeInnerAlpha), + "value": _formatPercent(activeInnerAlpha), }, ), ), @@ -390,14 +458,14 @@ class _ContentClassTablePageState extends State { context, "classtable.visual_settings.completed_section", ), - style: TextStyle(fontWeight: FontWeight.w700), + style: const TextStyle(fontWeight: FontWeight.w700), ), Text( FlutterI18n.translate( context, "classtable.visual_settings.completed_saturation_factor", translationParams: { - "value": formatPercent(completedSaturationFactor), + "value": _formatPercent(completedSaturationFactor), }, ), ), @@ -410,12 +478,30 @@ class _ContentClassTablePageState extends State { () => completedSaturationFactor = value, ), ), + Text( + FlutterI18n.translate( + context, + "classtable.visual_settings.completed_brightness_factor", + translationParams: { + "value": _formatPercent(completedBrightnessFactor), + }, + ), + ), + Slider( + value: completedBrightnessFactor, + min: 0.5, + max: 1.0, + divisions: 10, + onChanged: (value) => setDialogState( + () => completedBrightnessFactor = value, + ), + ), Text( FlutterI18n.translate( context, "classtable.visual_settings.completed_text_saturation_factor", translationParams: { - "value": formatPercent( + "value": _formatPercent( completedTextSaturationFactor, ), }, @@ -435,7 +521,7 @@ class _ContentClassTablePageState extends State { context, "classtable.visual_settings.completed_border_alpha", translationParams: { - "value": formatPercent(completedBorderAlpha), + "value": _formatPercent(completedBorderAlpha), }, ), ), @@ -452,7 +538,7 @@ class _ContentClassTablePageState extends State { context, "classtable.visual_settings.completed_inner_alpha", translationParams: { - "value": formatPercent(completedInnerAlpha), + "value": _formatPercent(completedInnerAlpha), }, ), ), @@ -487,14 +573,15 @@ class _ContentClassTablePageState extends State { return; } - CurrentTimeIndicatorConfig.enabled = enabled; - CurrentTimeIndicatorConfig.showTimeLabel = showTimeLabel; - CurrentTimeIndicatorConfig.showTodayColumnHighlight = - showTodayColumnHighlight; + CompletedClassStyleConfig.activeBrightnessFactor = activeBrightnessFactor + .clamp(0.5, 1.0) + .toDouble(); CompletedClassStyleConfig.activeBorderAlpha = activeBorderAlpha; CompletedClassStyleConfig.activeInnerAlpha = activeInnerAlpha; CompletedClassStyleConfig.completedSaturationFactor = completedSaturationFactor; + CompletedClassStyleConfig.completedBrightnessFactor = + completedBrightnessFactor.clamp(0.5, 1.0).toDouble(); CompletedClassStyleConfig.completedTextSaturationFactor = completedTextSaturationFactor; CompletedClassStyleConfig.completedBorderAlpha = completedBorderAlpha; @@ -585,7 +672,16 @@ class _ContentClassTablePageState extends State { child: Text( FlutterI18n.translate( context, - "classtable.popup_menu.visual_settings", + "classtable.popup_menu.current_time_settings", + ), + ), + ), + PopupMenuItem( + value: 'K', + child: Text( + FlutterI18n.translate( + context, + "classtable.popup_menu.class_color_settings", ), ), ), @@ -837,7 +933,10 @@ class _ContentClassTablePageState extends State { } break; case 'J': - await _showClassTableVisualSettingsDialog(); + await _showCurrentTimeSettingsDialog(); + break; + case 'K': + await _showClassColorSettingsDialog(); break; } }, 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 ff6eee42..ac9200ec 100644 --- a/lib/page/classtable/class_table_view/class_table_view.dart +++ b/lib/page/classtable/class_table_view/class_table_view.dart @@ -103,6 +103,11 @@ class _ClassTableViewState extends State { List classSubRow(bool isRest) { if (isRest) { List thisRow = []; + final currentDayColumnBox = _currentDayColumnBox(); + if (currentDayColumnBox != null) { + thisRow.add(currentDayColumnBox); + } + for (var index = 1; index <= 7; ++index) { List arrangedEvents = classTableState .getArrangement(weekIndex: widget.index, dayIndex: index); @@ -124,10 +129,6 @@ class _ClassTableViewState extends State { } final timeIndicator = _currentTimeIndicator(); - final currentDayColumnBox = _currentDayColumnBox(); - if (currentDayColumnBox != null) { - thisRow.add(currentDayColumnBox); - } if (timeIndicator != null) { thisRow.add(timeIndicator); } diff --git a/lib/page/classtable/class_table_view/completed_class_style.dart b/lib/page/classtable/class_table_view/completed_class_style.dart index 38138bf4..1db24e02 100644 --- a/lib/page/classtable/class_table_view/completed_class_style.dart +++ b/lib/page/classtable/class_table_view/completed_class_style.dart @@ -9,12 +9,14 @@ import 'package:watermeter/page/classtable/class_table_view/class_organized_data class CompletedClassStyleConfig { /// Completed-card color tuning. /// Lower values make finished classes look more muted and faded. - static double completedSaturationFactor = 0.5; + static double completedSaturationFactor = 0.25; + static double completedBrightnessFactor = 0.75; static double completedTextSaturationFactor = 0.75; static double completedBorderAlpha = 0.75; - static double completedInnerAlpha = 0.5; + static double completedInnerAlpha = 0.75; /// Active-card baseline appearance. + static double activeBrightnessFactor = 1.0; static double activeBorderAlpha = 1.0; static double activeInnerAlpha = 0.9; } @@ -41,28 +43,56 @@ class CompletedClassStyle { return hsl.withSaturation(hsl.saturation * factor).toColor(); } + static Color _adjustBrightness(Color color, {double factor = 1.0}) { + final hsl = HSLColor.fromColor(color); + final brightness = (hsl.lightness * factor).clamp(0.0, 1.0).toDouble(); + return hsl.withLightness(brightness).toColor(); + } + + static Color _tuneColor( + Color color, { + required double saturationFactor, + required double brightnessFactor, + }) { + final desaturated = _desaturateColor(color, factor: saturationFactor); + return _adjustBrightness(desaturated, factor: brightnessFactor); + } + static CompletedClassStyleData resolve({ required MaterialColor palette, required bool isCompleted, }) { + final saturationFactor = isCompleted + ? CompletedClassStyleConfig.completedSaturationFactor + : 1.0; + final brightnessFactor = + (isCompleted + ? CompletedClassStyleConfig.completedBrightnessFactor + : CompletedClassStyleConfig.activeBrightnessFactor) + .clamp(0.5, 1.0) + .toDouble(); final borderColor = isCompleted - ? _desaturateColor( + ? _tuneColor( palette.shade300, - factor: CompletedClassStyleConfig.completedSaturationFactor, + saturationFactor: saturationFactor, + brightnessFactor: brightnessFactor, ) - : palette.shade300; + : _adjustBrightness(palette.shade300, factor: brightnessFactor); final innerColor = isCompleted - ? _desaturateColor( + ? _tuneColor( palette.shade100, - factor: CompletedClassStyleConfig.completedSaturationFactor, + saturationFactor: saturationFactor, + brightnessFactor: brightnessFactor, ) - : palette.shade100; + : _adjustBrightness(palette.shade100, factor: brightnessFactor); final textColor = isCompleted - ? _desaturateColor( + ? _tuneColor( palette.shade900, - factor: CompletedClassStyleConfig.completedTextSaturationFactor, + saturationFactor: + CompletedClassStyleConfig.completedTextSaturationFactor, + brightnessFactor: brightnessFactor, ) - : palette.shade900; + : _adjustBrightness(palette.shade900, factor: brightnessFactor); return CompletedClassStyleData( borderColor: borderColor, diff --git a/lib/page/classtable/class_table_view/current_time_indicator.dart b/lib/page/classtable/class_table_view/current_time_indicator.dart index 80faa6f7..8d969da3 100644 --- a/lib/page/classtable/class_table_view/current_time_indicator.dart +++ b/lib/page/classtable/class_table_view/current_time_indicator.dart @@ -6,18 +6,18 @@ import 'package:watermeter/model/time_list.dart'; class CurrentTimeIndicatorConfig { static bool enabled = true; - static bool showTimeLabel = false; + static bool showTimeLabel = true; static bool showTodayColumnHighlight = true; static double lineAlpha = 0.9; static double lineThickness = 2; - static double labelHeight = 14; - static double labelFontSize = 9; - static double labelBackgroundAlpha = 0.45; - static double labelHorizontalPadding = 2; - static double labelVerticalPadding = 0; + static double labelHeight = 13; + static double labelFontSize = 8; + static double labelBackgroundAlpha = 0.75; + static double labelHorizontalPadding = 1; + static double labelVerticalPadding = 1; static double labelBorderRadius = 4; - static double dayColumnBorderAlpha = 0.65; - static double dayColumnBorderWidth = 2; + static double dayColumnHighlightAlpha = 0.25; + static double dayColumnHighlightRadius = 8; } class CurrentTimeIndicator { @@ -113,38 +113,56 @@ class CurrentTimeIndicator { final lineTop = blockHeight(_transferIndex(now)); final colorScheme = Theme.of(context).colorScheme; final color = colorScheme.primary; - final lineHorizontalInset = - CurrentTimeIndicatorConfig.showTodayColumnHighlight - ? CurrentTimeIndicatorConfig.dayColumnBorderWidth - : 0.0; final labelText = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; final hasLabel = CurrentTimeIndicatorConfig.showTimeLabel; final labelHeight = CurrentTimeIndicatorConfig.labelHeight; final indicatorTop = lineTop > labelHeight ? lineTop - labelHeight : 0.0; final lineOffset = lineTop - indicatorTop; + final lineTopOffset = + lineOffset - CurrentTimeIndicatorConfig.lineThickness / 2; + final labelTop = (lineOffset - CurrentTimeIndicatorConfig.labelHeight / 2) + .clamp(0.0, double.infinity) + .toDouble(); + final labelBottom = labelTop + CurrentTimeIndicatorConfig.labelHeight; + final lineBottom = lineTopOffset + CurrentTimeIndicatorConfig.lineThickness; + final indicatorHeight = + (hasLabel + ? (labelBottom > lineBottom ? labelBottom : lineBottom) + : lineBottom) + .clamp(0.0, double.infinity) + .toDouble(); final labelBackgroundColor = colorScheme.surface.withValues( alpha: CurrentTimeIndicatorConfig.labelBackgroundAlpha, ); + final connectorColor = color.withValues( + alpha: CurrentTimeIndicatorConfig.lineAlpha * 0.35, + ); + final lineColor = color.withValues( + alpha: CurrentTimeIndicatorConfig.lineAlpha, + ); return Positioned( - left: leftRow + blockWidth * dayOffset, + left: 0, top: indicatorTop, - width: blockWidth, + width: leftRow + blockWidth * (dayOffset + 1), child: IgnorePointer( child: SizedBox( - height: lineOffset + CurrentTimeIndicatorConfig.lineThickness, + height: indicatorHeight, child: Stack( children: [ if (hasLabel) - Align( - alignment: Alignment.topCenter, + Positioned( + top: labelTop, + left: 0, + width: leftRow, + height: CurrentTimeIndicatorConfig.labelHeight, child: DecoratedBox( decoration: BoxDecoration( color: labelBackgroundColor, border: Border.all( - color: color.withValues(alpha: 0.2), - width: 0.6, + color: color.withValues(alpha: 0.7), + width: 1.4, ), borderRadius: BorderRadius.circular( CurrentTimeIndicatorConfig.labelBorderRadius, @@ -157,33 +175,44 @@ class CurrentTimeIndicator { vertical: CurrentTimeIndicatorConfig.labelVerticalPadding, ), - child: Text( - labelText, - style: TextStyle( - fontSize: CurrentTimeIndicatorConfig.labelFontSize, - color: color, - fontWeight: FontWeight.w700, - shadows: const [ - Shadow( - offset: Offset(0, 0), - blurRadius: 2, - color: Colors.black26, - ), - ], + child: Center( + child: Text( + labelText, + style: TextStyle( + fontSize: CurrentTimeIndicatorConfig.labelFontSize, + color: color, + fontWeight: FontWeight.w700, + height: 1, + shadows: const [ + Shadow( + offset: Offset(0, 0), + blurRadius: 2, + color: Colors.black26, + ), + ], + ), ), ), ), ), ), + if (dayOffset > 0) + Positioned( + top: lineTopOffset, + left: leftRow, + width: blockWidth * dayOffset, + child: Container( + height: CurrentTimeIndicatorConfig.lineThickness, + color: connectorColor, + ), + ), Positioned( - top: lineOffset - CurrentTimeIndicatorConfig.lineThickness / 2, - left: lineHorizontalInset / 2, - right: lineHorizontalInset / 2, + top: lineTopOffset, + left: leftRow + blockWidth * dayOffset, + width: blockWidth, child: Container( height: CurrentTimeIndicatorConfig.lineThickness, - color: color.withValues( - alpha: CurrentTimeIndicatorConfig.lineAlpha, - ), + color: lineColor, ), ), ], @@ -217,23 +246,20 @@ class CurrentTimeIndicator { } final color = Theme.of(context).colorScheme.primary.withValues( - alpha: CurrentTimeIndicatorConfig.lineAlpha, + alpha: CurrentTimeIndicatorConfig.dayColumnHighlightAlpha, ); return Positioned( - left: - leftRow + - blockWidth * dayOffset - - CurrentTimeIndicatorConfig.dayColumnBorderWidth / 2, + left: leftRow + blockWidth * dayOffset, top: 0, - width: blockWidth + CurrentTimeIndicatorConfig.dayColumnBorderWidth, + width: blockWidth, height: blockHeight(61), child: IgnorePointer( child: DecoratedBox( decoration: BoxDecoration( - border: Border.all( - color: color, - width: CurrentTimeIndicatorConfig.dayColumnBorderWidth, + color: color, + borderRadius: BorderRadius.circular( + CurrentTimeIndicatorConfig.dayColumnHighlightRadius, ), ), ), From 96273b2146ce1da94a4a8c13249dad5409701bae Mon Sep 17 00:00:00 2001 From: SolitaryDream-X Date: Sat, 18 Apr 2026 20:41:26 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix(global=5Ftimer):=20=E5=B0=86=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E5=99=A8=E9=97=B4=E9=9A=94=E4=BB=8E15=E7=A7=92?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E4=B8=BA1=E5=88=86=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/controller/global_timer_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controller/global_timer_controller.dart b/lib/controller/global_timer_controller.dart index 732d3d06..708ec506 100644 --- a/lib/controller/global_timer_controller.dart +++ b/lib/controller/global_timer_controller.dart @@ -9,7 +9,7 @@ import 'package:watermeter/repository/logger.dart'; class GlobalTimerController { static final GlobalTimerController i = GlobalTimerController._(); GlobalTimerController._() { - _timer = Timer.periodic(const Duration(seconds: 15), (_) { + _timer = Timer.periodic(const Duration(minutes: 1), (_) { currentTimeSignal.value = DateTime.now(); log.debug("Global Timer: Time is ${currentTimeSignal.value}"); });