Skip to content

[feat] 为添加课程页面提供更自由的时间选项#128

Open
hazuki-keatsu wants to merge 15 commits intomainfrom
feat/improveAddingCourse
Open

[feat] 为添加课程页面提供更自由的时间选项#128
hazuki-keatsu wants to merge 15 commits intomainfrom
feat/improveAddingCourse

Conversation

@hazuki-keatsu
Copy link
Copy Markdown
Collaborator

@hazuki-keatsu hazuki-keatsu commented Apr 17, 2026

新增自定义课程类支持,包含对应的数据模型、控制器与界面集成;为所有支持的语言添加了月份翻译,并完善课程表相关的本地化文案;同时包含若干代码优化与问题修复(例如,使用散列空间更大的通知ID生成器)。

Closes: #116

- Added CustomClass and CustomClassTimeRange models for handling custom classes and their time ranges.
- Introduced CustomClassController to manage the lifecycle of custom classes, including adding, editing, and deleting classes and their time ranges.
- Updated ClassTableWidgetState to integrate custom class data and provide methods for managing custom classes.
- Enhanced ClassCard to support displaying and interacting with custom classes.
- Created CustomClassDetailCard for detailed view and actions on custom classes.
- Implemented DateSelectorFree for selecting date ranges and time slots for classes.
- Updated class organization logic to include custom classes in the timetable.
- Refactored existing code to accommodate new custom class features and ensure smooth integration with the existing class table functionality.
Copy link
Copy Markdown
Owner

@BenderBlog BenderBlog left a comment

Choose a reason for hiding this comment

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

我接下来一周会忙其他的事情,所以您先慢慢改。现在没有发版本压力,所以不着急。

Comment thread lib/controller/custom_class_controller.dart
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

仅需截图这个卡片长啥样。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

image

卡片如上图

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

我看见上面有“时间数组”,需要来一张有多个时间段的

Comment thread lib/page/classtable/classtable_state.dart
hazuki-keatsu and others added 4 commits April 28, 2026 15:50
Co-authored-by: Copilot <copilot@github.com>
…s file

- Introduced a new `clearAll` method in `CustomClassController` to clear all custom class data.
- Updated `CustomClassController` to filter out invalid entries when loading custom classes.
- Removed the `UserDefinedClassFile` class and its associated methods, consolidating functionality into `CustomClassController`.
- Updated various controllers and pages to utilize the new custom class management system.
- Added a Swift model for custom classes to support iOS widget integration, and so did Android.
@hazuki-keatsu
Copy link
Copy Markdown
Collaborator Author

已经完成的工作:

  1. 新增CustomClass数据模型及控制器的支持
  2. 全面替换原有的UserDefinedClass的数据流,使用新的CustomClass数据流
  3. 清除所有的与UserDefinedClass相关的代码
  4. CustomClass数据流对接ClassTableStateClassAddWindowHomepageControllerCourseReminderServiceSystemCalendarSyncService以及Android和iOS的小组件

剩余工作:

  1. iOS平台上的小组件没有测试
  2. 在测试时,ContentClassTablePageState出现了setState() after dispose()的报错,这个报错我没有找到报错的原因和复现的方法,应该不是由于我的修改引起的,所以暂时忽略。

与原计划不同的地方:

我完全放弃了在添加课程界面对周重复索引的支持相关回复,由于新的数据结构使用的是DateTime,而旧的数据结构使用的是索引,两者之间的信息密度不对等,意味着向下进行转换一定会有信息的丢失。保证信息准确性,UI的展示就不准确;保证UI展示的准确性,信息就会有偏差,所以我干脆放弃这块的转换,避免不必要的问题。

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

本人TODO:我得查查这玩意代码咋简化,这时间序列化感觉不应如此复杂。

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

这个是我的事情,您不用干。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

确实没必要这么复杂,这个代码是AI写的,做了很多的降级处理。您看看这个怎么修改吧

@BenderBlog
Copy link
Copy Markdown
Owner

  1. 大致查看了相关控制器,先按你的来吧,因为现在用户添加的信息跟课程表无关了。
  2. 文件处理一律在 repository 里面处理,文件处理放心使用同步方法,我认为的整套流程:
    • 用户触发添加信息 / 修改信息 / 删除信息;
    • 先在控制器里面对列表进行修改;
    • 将修改后的列表通过 repository 里面的方法写入文件。
    • 附加清除列表:在学期刚开始或者用户请求的情况下,清除列表删除文件。
  3. 需要让 AI 写个总结文档。

@BenderBlog
Copy link
Copy Markdown
Owner

ContentClassTablePageState 这块,应该仅在热重载时候触发,对日常使用无影响。热重载时候先把输入课程信息页面退了吧。

@hazuki-keatsu
Copy link
Copy Markdown
Collaborator Author

  1. 大致查看了相关控制器,先按你的来吧,因为现在用户添加的信息跟课程表无关了。

  2. 文件处理一律在 repository 里面处理,文件处理放心使用同步方法,我认为的整套流程:

    • 用户触发添加信息 / 修改信息 / 删除信息;
    • 先在控制器里面对列表进行修改;
    • 将修改后的列表通过 repository 里面的方法写入文件。
    • 附加清除列表:在学期刚开始或者用户请求的情况下,清除列表删除文件。
  3. 需要让 AI 写个总结文档。

好的,那我就这两天再改改

@hazuki-keatsu
Copy link
Copy Markdown
Collaborator Author

  1. 大致查看了相关控制器,先按你的来吧,因为现在用户添加的信息跟课程表无关了。

  2. 文件处理一律在 repository 里面处理,文件处理放心使用同步方法,我认为的整套流程:

    • 用户触发添加信息 / 修改信息 / 删除信息;
    • 先在控制器里面对列表进行修改;
    • 将修改后的列表通过 repository 里面的方法写入文件。
    • 附加清除列表:在学期刚开始或者用户请求的情况下,清除列表删除文件。
  3. 需要让 AI 写个总结文档。

上面提到的先修改列表后写入文件,我觉得不太稳妥,因为文件是同步写入的,如果文件写入失败的话,列表就不会更新,这样可以一定程度上保证数据内容的一致。

然后我完成了将文件IO重构到repository的工作。

以下是更改总结,来自deepseek v4 pro:


提示词:

你能帮我把从7c679743a43cc49888f271b7dc70dd9952fd56b5到d752d63e6b090ef2c863801e7dda41576937d444的总结一下,里面有一些commit是我合并了其他的分支的最新内容,你主要关注对CustomClass相关的修改,并出具一个md格式的文件作为总结,同时评价一下这个代码的情况


CustomClass 功能演进总结

范围: d752d63e7c679743 (feat/improveAddingCourse 分支)
分析日期: 2026-05-03


一、提交概览

本分支包含 14 个提交(11 个功能提交 + 3 次 main 合并),围绕自定义课程功能的实现、集成和重构展开。

1.1 功能提交(按时间顺序)

提交 日期 说明
d752d63e 03-20 feat: 实现自定义课程管理功能(基础实现)
7d3cfee0 fix: 修复桌面平台通知服务初始化错误
661adb6b feat: 为日期选择器添加删除功能
2ec619a0 feat: 将 CustomCourseData 集成到课程提醒
d65964dc feat: 为日期选择器添加国际化,调整添加窗口布局
9d928bb7 refactor: 重构 CourseID 生成方式避免冲突
934c51ac fix: 更新 ID 生成器
f3f77398 fix: 避免开始时间等于结束时间,SnackBar 改为 Toast
f1a59652 05-01 refactor: 重构 custom class 处理,移除 UserDefinedClassFile
61c5cc61 fix: 对齐 Reminder 逻辑并修改日志内容
a18b7dc1 refactor: DateSelectorFree 样式微调
7c679743 05-03 refactor: 将文件 I/O 抽取到 CustomClassRepository

1.2 Main 分支合并

提交 说明
5bcd78cb Merge branch 'main'(早期)
fb0cde9d Merge branch 'main'(中期,引入 completed_class_style、current_time_indicator)
4046bf7f Merge branch 'main'(后期,包含 a38f3297 课表视觉改进 PR #129

二、架构演进

2.1 初始架构(d752d63e)

CustomClass (Model)  ←→  CustomClassController (文件 I/O + 状态管理)
                              │
                              ├── ClassAddWindow (添加/编辑 UI)
                              ├── DateSelectorFree (日期范围选择)
                              ├── ClassCard (课表卡片展示)
                              └── CustomClassDetailCard (详情/操作)
  • Controller 直接使用 dart:io File 读写 JSON
  • 文件路径硬编码在 controller 中
  • 包含旧的 UserDefinedClassFile 类(遗留代码)

2.2 重构后架构(f1a59652 → 7c67974

CustomClass (Model)
CustomClassTimeRange (Model)
        │
        ▼
CustomClassRepository (纯文件 I/O + iOS Widget 同步)
        │
        ▼
CustomClassController (业务逻辑 + Signal 状态管理)
        │
        ├── ClassAddWindow + DateSelectorFree (添加/编辑 UI)
        ├── ClassCard + CompletedClassStyle (课表卡片 + 进度可视化)
        ├── CurrentTimeIndicator (当前时间指示器)
        ├── CustomClassDetailCard (详情/操作)
        └── CourseReminder (提醒集成)

关键变化:

  1. 移除了 UserDefinedClassFile(~80 行代码删除),功能合并到 CustomClassController
  2. 新增 CustomClassRepository,把文件读写从 controller 剥离出来
  3. 新增 clearAll() 方法,学期切换时清空数据
  4. 新增 iOS/Android Widget 原生模型(Swift/Kotlin)
  5. 引入 CompletedClassStyleCurrentTimeIndicator(来自 main 的视觉改进)

2.3 数据流

用户操作 (添加/编辑/删除)
  │
  ▼
CustomClassController (signal-based)
  ├── 拷贝内存列表
  ├── 修改拷贝
  └── _save(updated)
        │
        ▼
      CustomClassRepository
        ├── 同步写入 JSON 文件
        └── iOS: 异步同步到 App Group (Widget 可读)

三、涉及文件

3.1 新增文件

文件 说明
lib/model/pda_service/custom_class.dart CustomClass + CustomClassTimeRange 模型
lib/controller/custom_class_controller.dart 自定义课程控制器(单例,signal 状态管理)
lib/repository/custom_class_repository.dart 文件 I/O 层(load/save/delete + iOS 同步)
lib/page/classtable/class_add/date_selector_free.dart 自由日期范围选择器
lib/page/classtable/class_table_view/completed_class_style.dart 已完成课程样式解析(来自 main)
lib/page/classtable/class_table_view/current_time_indicator.dart 当前时间指示线(来自 main)
lib/page/classtable/arrangement_detail/custom_class_detail_card.dart 自定义课程详情卡片
ios/ClasstableWidget/CustomClassModel.swift iOS Widget 数据模型
Android: ClassTableModels.kt 中的 CustomClass 扩展 Android Widget 数据模型

3.2 重大修改文件

文件 变化量 说明
class_card.dart 386 行变更 多次合并冲突解决,集成 Stack 进度条 + CustomClass 交互
class_add_window.dart 331 行变更 重构构造函数参数 (customToChange),布局调整
class_organized_data.dart 106 行变更 新增 CustomClass 数据组织和颜色映射
classtable_state.dart 72+64 行删减 重构方法,移除 UserDefinedClass 相关代码
class_table_view.dart 74 行新增 集成 completedHeight 和 currentTimeIndicator

四、代码质量评估

4.1 优点

  • 分层清晰:Model → Repository → Controller → UI,职责明确,controller 不再直接碰文件
  • 同步文件 I/O 选择合理:本地小数据量的 JSON 读写用同步方法,逻辑简单可靠
  • 模型验证严格CustomClass 构造时检查 name 非空、timeRanges 非空;CustomClassTimeRange 检查时间合法性(08:30-21:25、同天、start < end)
  • 错误处理完善_load() 过滤损坏条目并记录日志,不会因单条数据损坏导致整体加载失败;状态机清晰(fetching → fetched / error)
  • 响应式状态管理:使用 signals 包,UI 自动响应数据变化
  • 跨平台 Widget 支持:iOS 通过 Pigeon + App Group,Android 直接读文件
  • 学期切换自动清理SemesterControllerClassTableController effect → clearAll(),无需用户手动操作

4.2 待改进

问题 严重程度 说明
ClassOrgainzedData.dataList<dynamic> 缺乏类型安全,ClassCard 中需要大量 is 类型判断和强制转换
Controller 方法返回 Future<void> 但内部全部同步 实际是同步操作,Future 是多余的;调用方也被迫 await
_save() 返回 bool 但所有调用方忽略 成功/失败状态通过 signal 传递,返回值从未使用
Repository 中 iOS 同步使用 .then().catchError() 而非 unawaited 虽然功能上等效,但 unawaited 语义更明确
ClassCard.build() 方法较长 嵌套的 LayoutBuilder + Stack + TextButton.onPressed,可读性一般;但考虑到复杂布局,属可接受范围
合并冲突处理 三次 main 合并带来了复杂的冲突(class_card.dart 中冲突标记交叉嵌套),说明分支与 main 的偏离较大

4.3 总体评价

代码整体质量 良好。从初始实现到两轮重构,架构越来越清晰:

  • 初始版本功能完备但 controller 职责过重
  • 第一轮重构(f1a59652)消除了 UserDefinedClassFile 遗留代码,净删除 313 行
  • 第二轮重构(7c679743)补齐了 repository 层,实现了用户期望的"controller 管业务、repository 管文件"的分层

主要技术债务是 List<dynamic> 的类型安全问题,以及 Future<void> 的语义不一致。这些问题不影响功能,但后续如果有时间可以做一次类型安全化改造。

const val EXAM_FILE_NAME = "exam.json"
const val PHYSICS_EXPERIMENT_FILE_NAME = "PhysicsExperiment.json"
const val OTHER_EXPERIMENT_FILE_NAME = "OtherExperiment.json"
const val CUSTOM_CLASS_FILE_NAME = "CustomClassesV2.json"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

按照我的要求,直接使用新结构体。这是在安卓小部件方面的修改。

val termStartDay: String,
val classDetail: List<ClassDetail>,
val userDefinedDetail: List<ClassDetail>,
val userDefinedDetail: List<ClassDetail> = emptyList(),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

旧版本的记录清除,这是在安卓小部件方面的修改。

Source.EXAM -> "Unknown Exam"
Source.EXPERIMENT -> "Unknown Experiment"
Source.EMPTY -> "Unknown Empty"
Source.USER -> "Unknown Custom Class"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

安卓小部件方面,理论上不应该报错的东西(至少我这么写的注释)

}
}

@Serializable
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

记得 Kotlin 要想解析 Dart 的 Record 对象要这么搞?需要核实下面的代码。


otherExperimentJsonData = loadFileContent(
context, ClassTableConstants.OTHER_EXPERIMENT_FILE_NAME
customClassJsonData = loadFileContent(
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

好吧,就是改了加载新的用户定义日程信息代码。

val termStartDayStr = classTableData.termStartDay
if (termStartDayStr.isBlank()) {
if (classTableData == ClassTableData.EMPTY && userDefinedClassData == UserDefinedClassData.EMPTY) {
if (classTableData == ClassTableData.EMPTY) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

移除老的用户定义课程信息。

}
}
} No newline at end of file

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

加载某一天的用户定义课程信息。日期由总体状态 currentTime 定义

}
}

struct UserDefinedClassData : Codable {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

没错,不搞老的数据了。

var semesterCode : String
var termStartDay : String
var classDetail : [ClassDetail]
var userDefinedDetail : [ClassDetail]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

需要观察接下来这玩意咋搞的,userDefinedDetail 现在已经作废了

private let widgetGroupId = "group.xyz.superbart.xdyou"
private let classTableFile = "ClassTable.json"
private let userClassFile = "UserClass.json"
private let customClassFile = "CustomClassesV2.json"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

开始看看 ios 组件,咋改的这个部分,尤其要回到 Dart 代码里面看看同步过来没有?

var timeArrangement : [TimeArrangement]
var classChanges : [ClassChange]


Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

这……有必要吗

return
}


Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

添加方式,我看没有问题


import 'package:intl/intl.dart';
import 'package:signals/signals.dart';
import 'package:watermeter/controller/custom_class_controller.dart';
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

没错,现在用户添加的日程需要单独放到一个控制器管理了,因为和课程表没关系了。

userDefinedClassSignal.value = UserDefinedClassData.empty();
ClassTableSession.deleteCache();
UserDefinedClassFile.clearUserDefinedClass();
unawaited(CustomClassController.i.clearAll());
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

有必要上 unawaited 吗,需要看看这个控制器里面的方法。因为都是对小文件的读写,用同步方法没有任何问题。

@@ -34,102 +34,6 @@ class ClassTableController {

SemesterSyncEvent? _lastHandledSemesterSyncEvent;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

对,别在这里搞对用户日程的增删改查了。

@@ -191,22 +94,18 @@ class ClassTableController {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

继续删除之前的用户自定义课程合并逻辑,很好


import 'package:signals/signals.dart';
import 'package:watermeter/model/pda_service/custom_class.dart';
import 'package:watermeter/repository/custom_class_service.dart';
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

估计所有的实际操作文件方法都在这里。

import 'package:watermeter/repository/custom_class_service.dart';
import 'package:watermeter/repository/logger.dart';

enum CustomClassState { fetching, fetched, error, none }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

后期可能需要优化,是不是可以不要这些玩意了。

}

/// 通过周索引、日索引和学期开始日期来找到有日程的那天
List<CustomClassOccurrence> getOccurrenceOfDay({
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

感觉有些复杂了,直接存日期就完了啊……

@@ -1,15 +1,17 @@
// Copyright 2026 Traintime PDA Authours, originally by BenderBlog Rodriguez.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

没问题的修改,读取新的用户日程方式

final List<CustomClassTimeRange> timeRanges;

CustomClass({
required this.id,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

这个 ID 是干啥用的

final String? teacher;
final String? classroom;
@JsonKey(name: 'time_ranges')
final List<CustomClassTimeRange> timeRanges;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

这个好,后续我要重写 XDYou 的话我会这么考虑新的课程结构体的。因为之前的设计完全按照数据库来的:P

part 'classtable.g.dart';

enum Source { empty, school, user }
enum Source { empty, school }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

没问题,现在所有课程来源全是学校了

);
} else if (classDetailState.information[i]
is (CustomClass, CustomClassTimeRange)) {
// 颜色降级
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

明白,新的用户日程卡片

elevation: 0,
color: infoColor.shade100,
child: Container(
padding: EdgeInsets.fromLTRB(
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

需要截图看看这个,如果之前截图了再截图一次吧:P

@@ -4,18 +4,19 @@

import 'package:flutter/material.dart';
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

新页面代码,我就按照之前截图来确认了

@@ -0,0 +1,543 @@
// Copyright 2026 Hazuki Keatsu.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

上注释,告诉我这是啥东西

/// IDs stay in the same numeric bucket so `isCourseReminderNotificationId`
/// continues to work, while removing random collisions from parallel
/// scheduling.
int _generateNotificationId(String uniqueKey) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

没看懂,需要解释。

}
}

void _syncToWidget(String data) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

没问题

Comment thread lib/main.dart
// Initialize notification services
try {
await NotificationServiceRegistrar().initializeAllServices();
if (Platform.isAndroid || Platform.isIOS) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

行吧,仅手机可以搞通知。电脑搞通知没啥意义。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 为添加课程页面提供更自由的时间选项

2 participants