Skip to content

Add Course

lingion edited this page Sep 12, 2026 · 1 revision

Course Editor

TL;DR:手动新建和编辑课程的表单页。字段默认值、时段卡结构、非常规槽位、保存与撤回流程全部对照 v1.0.54 源码写成。

本文基于 v1.0.54(versionCode 58)。核心源码 app/src/main/java/com/lingion/sleepy/ui/screen/edit/AddCourseScreen.kt,下文简写 AddCourseScreen.kt;行号锚点无前缀的,相对 app/src/main/java/com/lingion/sleepy/。

入口与双模式

三个入口,汇到同一个 AddCourseScreen:

  • 课表页、管理页的「手动添加」,新建模式(MainActivity.kt:495、MainActivity.kt:508)
  • 点已有课程(课表页 / 今日页 / 管理页列表),编辑模式(MainActivity.kt:408、MainActivity.kt:497、MainActivity.kt:500)
  • 通知深层链接直接打开对应课程进编辑(MainActivity.kt:242)

区别只在 editingCourse 参数:空走新建,非空走编辑(AddCourseScreen.kt:187-191)。顶栏标题随之切换「手动创建课程」/「编辑课程」(app/src/main/res/values-zh-rCN/strings.xml:222-223),保存按钮切换「创建课程」/「保存课程」(strings.xml:272、strings.xml:247)。保存成功后退出表单并切到课表 tab(MainActivity.kt:285)。

新建模式的字段默认值:

字段 默认值 代码
课程名 / 别名 空 AddCourseScreen.kt:224-226
整课起始周 / 结束周 1 / 16 AddCourseScreen.kt:228-229
时段卡星期 周一 AddCourseScreen.kt:730-739
开始节 / 连上几节 1 / 2 同上
卡内覆盖起止 08:00–09:40 同上
周类型 每周 AddCourseScreen.kt:136
颜色模式 GROUP(跟组色) AddCourseScreen.kt:141

「新增一个上课时段」追加的卡另有一套默认:周二、第 3 节连 2 节、覆盖起止 10:00–11:40、周次 1–16(AddCourseScreen.kt:632-643)。

表单四段

页面是一个纵向列表,从上到下:校验红卡(有问题才出现)、课程基础信息卡、周次范围卡、上课时段卡列表、保存按钮、删除按钮(仅编辑模式)(AddCourseScreen.kt:498-713)。

┌─ 校验红卡(有问题才出现,最多显 4 条)─────────┐
├─ 课程基础信息 ── 课程名 * · 别名(可选)──────┤
├─ 周次范围 ────── 起始周 | 结束周             │
│                  [应用到所有时段]            │
├─ 上课时段 ────── 时段 1 ┐                    │
│                  时段 2 ├ 每卡独立,可增删    │
│                  时段 N ┘                    │
│                  [+ 新增一个上课时段]        │
├─ [✓ 创建课程 / 保存课程]                    │
└─ 「删除」─────────────────────┘

课程基础信息

  • 课程名必填,输入框标签「课程名 *」;别名可选(AddCourseScreen.kt:512-530)。
  • 别名是展示名。生效范围只有显示场景:周视图、网格视图、桌面组件编辑各有「显示别名」开关,开关打开且别名非空才替换原名;课名匹配、导出、通知、详情页一律读原名,别名留空则处处显原名(util/CourseDisplayUtil.kt:5-22、res/values-zh-rCN/strings.xml:334-335)。
  • 别名按组保存:保存时写进该组每一行,整组一致(AddCourseScreen.kt:225-226、AddCourseScreen.kt:318-323)。

周次范围

  • 起始周、结束周,输入范围 1..30(AddCourseScreen.kt:545-562)。
  • 这组值本身不直接落库,每张卡有自己的周次;「应用到所有时段」按钮把这两个值一键覆盖到全部时段卡,不点就不动卡(AddCourseScreen.kt:564-577、AddCourseScreen.kt:803-804)。
  • 所有数字输入框共用 NumberField:超界静默夹紧到上下界,清空回落最小值,非数字字符不回调(AddCourseScreen.kt:1718-1767)。时段卡的节次字段被夹紧时,卡片底色转 errorContainer 并提示去课表管理改节数(AddCourseScreen.kt:166、AddCourseScreen.kt:1058-1077、strings.xml:264)。

上课时段

时段卡是录入单元:一张卡 = 多选星期 + 一种排课方式(节次或时间)+ 卡内周次 / 老师 / 地点 / 备注 / 颜色。落库时卡内每个选中星期各生成一行课程,共享这组属性(AddCourseScreen.kt:314-328)。卡数不限,至少保留一张;卡头 X 图标删除当前卡(AddCourseScreen.kt:606、strings.xml:251)。

时段卡逐字段

标准节次

  • 「开始第几节」+「连上几节」。上界 = 当前课表最大连续节次 maxStd(util/TimeTableUtils.kt:557);开始节 + 连上数 − 1 越过 maxStd,保存被拒(AddCourseScreen.kt:850-856)。
  • 调大开始节时,连上数自动收到上界内(AddCourseScreen.kt:1123-1125)。

非常规选项折叠栏

「非常规节次」「非常规时间」两个开关收进同一个折叠区,默认收起;编辑一张已启用任一项的卡时自动展开(AddCourseScreen.kt:163、AddCourseScreen.kt:1237-1242)。收起但选项开着时,栏头露出摘要,比如「非常规节次 · 第 0 节」或「非常规时间 · 18:30–20:10」(AddCourseScreen.kt:916-940)。

开关一「非常规节次」:把这张卡绑到课表边缘槽位(第 0 节 / 第 -1 节 / 第 N+1 节)。开启立即弹候选层;关闭释放槽位,若槽位是本卡新建且无他卡引用,暂存项一并回收(AddCourseScreen.kt:1325-1341、AddCourseScreen.kt:616-624)。

开关二「非常规时间」:覆盖本卡起止,可完全落在槽位或节次时间窗之外。首次开启预填当前生效时间(槽位默认或标准节次时间);「开始时间」「结束时间」「持续时长(分钟)」三个输入联动,最后编辑的一对为准;时长反推跨午夜时回退清空,交给校验报错(AddCourseScreen.kt:1378-1453)。

候选槽位弹层

候选集合分两组:Before 组(前置,节点号 ≤ 0)与 After 组(后置,大于 maxStd),各组 = 该方向全部已有槽位(带默认时间)+ 紧贴边界的一个「新建」候选,禁止跳号(util/TimeTableUtils.kt:572-589)。已有槽位高亮,显示「第 N 节 18:00 – 18:45」;新建候选显示「第 N 节(新建)」(AddCourseScreen.kt:1462-1512、strings.xml:703、strings.xml:709)。

一张卡最终用什么时间,解析顺序固定,校验、落库、卡间重叠检测共用同一份实现(util/TimeTableUtils.kt:529-554):

  1. 勾了「非常规时间」→ 卡上覆盖起止直接生效,不受任何槽位时间窗约束;
  2. 否则绑了边缘槽位 → 槽位默认时间;
  3. 否则 → 标准节次时间(开始节到开始节 + 连上数 − 1 拼接)。

解析失败(时间非法 / 节次不存在)返回空,由校验报错,不静默给值。

新建槽位流程

点「新建」候选,弹出对话框填起止时间,两个时间都能解析才可确认(AddCourseScreen.kt:1516-1552)。

确认后槽位进 pendingEdgeInserts 暂存列表,这只是 UI 工作副本,不写课表 timeJson。此期间候选集合、时间展示、校验全部基于「基础 timeJson + 暂存改动」合并出的生效时间表,所见即所存(AddCourseScreen.kt:94-101、AddCourseScreen.kt:210-222)。暂存项记录用户所点的候选编号;真正落库时由 insertEdgeNode 按方向重新分配节点号:Before 方向取 0 或现有最小值 − 1,After 方向取 maxStd + 1 或现有最大值 + 1,一次保存多个槽位按顺序串接、编号连续(util/TimeTableUtils.kt:456-478)。点保存且成功后,暂存槽位才一次性写回课表(AddCourseScreen.kt:369-387)。

槽位默认时间编辑

已有槽位在卡内显示摘要,旁边的编辑入口弹「编辑第 N 节默认时间」对话框(AddCourseScreen.kt:1366-1374、AddCourseScreen.kt:1554-1594)。改的是课表 timeJson 里的那一行,对该槽位的全部引用课程全局生效(AddCourseScreen.kt:103-109、util/TimeTableUtils.kt:590-602)。标准节次 1..N 的时间不在此改:非边缘行原样返回。同一节点多次编辑,取最后一次(AddCourseScreen.kt:462-464)。

每卡周次与周类型

  • 每张卡有独立的起始周 / 结束周,1..30(AddCourseScreen.kt:1152-1175)。「应用到所有时段」只覆盖周次,不动周类型。
  • 周类型四态:「每周」「单周」「双周」「按周次」(AddCourseScreen.kt:1176-1188)。「按周次」(type=3)来自导入的单次实验课数据,编辑器没有逐周勾选入口,原样回填与保存。冲突判定里 type=3 与「每周」同规则:公共区间内即命中(util/WeekRangeOverlap.kt:12-16)。

老师 / 地点 / 备注

三个字段每卡独立(AddCourseScreen.kt:1190-1218)。同一门课在不同教室或由不同老师上,各占一张卡,互不覆盖。

颜色三态

「颜色」行带一个开关:关 = 跟随组色,开 = 使用不同颜色(AddCourseScreen.kt:1596-1680)。

  • GROUP(默认):整组同色。编辑器里这一档不产生自己的颜色,保存时空色回落哨兵值;渲染先看本行 color 字段,非空白且非哨兵值(如 WakeUp 导入的组色)直接用它,否则按 groupId 哈希在色环上撒相,同组永远同色(util/CourseColorUtil.kt:226-235)。
  • AUTO:开关首次打开落在这一档。同组内按行序 × 137.508° 逐行推进色相,各时段卡颜色不同(util/CourseColorUtil.kt:281-303)。
  • CUSTOM:点色块弹 ColorPickerDialog,每卡独立选固定 hex,初始 #FF6750A4;确认后模式自动切到 CUSTOM 并显示色值(AddCourseScreen.kt:1669-1679)。

保存时 CUSTOM 写入选定色(留空回落 #FF6750A4),AUTO 存空串、渲染时现算(AddCourseScreen.kt:774-780)。

编辑回填

进编辑模式后,按 groupId 拉出该组全部行,按完整特征分组回填成多张卡(AddCourseScreen.kt:245-292)。分组键:

ownTime | startNode | step | startTime | endTime | startWeek | endWeek | type | room | teacher

(AddCourseScreen.kt:725-728)。day 不进键:同组出现过的星期去重排序后并进同一张卡,其余字段取组内第一行(AddCourseScreen.kt:265-283)。同名课多地点、多老师、不同周次,键不同,各成一卡。

回填联动:startNode 落在边缘槽位 → 自动点亮「非常规节次」;ownTime=true → 点亮「非常规时间」,预填覆盖起止并补算时长(AddCourseScreen.kt:255-288)。

注意两套键的分工。回填分组键决定「拆几张卡」;落库身份键 RowKey(day|startNode|step|startWeek|endWeek|type|room|teacher)决定「是不是同一行」。颜色、备注、别名不在 RowKey 里,改它们是原地 update,行不动(data/diff/RowKey.kt:14-23、data/diff/RowKeyDiffer.kt:75-89)。

保存流程

保存按钮在课名为空或没有时段卡时置灰(AddCourseScreen.kt:294、AddCourseScreen.kt:659-660)。点保存走六步:

  1. 校验。课名非空、整课周次为正;每卡:星期至少一天、起始周 ≤ 结束周;非常规节次卡的槽位必须真实存在,标准卡节次不得越过 maxStd;勾了非常规时间的卡检查时间格式与先后。最后做卡间两两重叠检测:同星期、生效时间区间相交(按分钟折算)、且周类型在公共周内同时命中,才算撞(AddCourseScreen.kt:816-893、util/WeekRangeOverlap.kt:5-22)。失败:顶部红卡列出问题(最多显 4 条,余下折叠计数),对应卡红底加卡内红字,不落库(AddCourseScreen.kt:500-504、AddCourseScreen.kt:943-973、AddCourseScreen.kt:1223-1233)。
  2. 无表自动建表。当前没有课表时先 createEmptyTable() 建一张再继续(AddCourseScreen.kt:330-333、ui/screen/schedule/ScheduleViewModel.kt:171-183)。
  3. ownTime 反算真实节点。勾了非常规时间的卡(非边缘槽位卡)落库前用 timeToNode 把覆盖起止映射回真实节次区间,今日页、桌面组件这类只认节点的消费方拿不到占位节点;映射失败保原值(AddCourseScreen.kt:785-791、util/TimeTableUtils.kt:106-139)。
  4. 非阻塞冲突检查。与本表其他课程比对,编辑模式排除本组旧记录(自己不算撞自己),命中弹明细:星期、节次、周次、撞哪门课,最多显 6 条(AddCourseScreen.kt:338-356、AddCourseScreen.kt:394-424、strings.xml:266-268)。「仍然保存」跳过检查直接落库;「返回修改」放弃本次。明细列表用 rememberSaveable 持有,屏幕旋转后弹窗还在(AddCourseScreen.kt:237-239)。
  5. 落库。编辑模式:RowKeyDiffer 对草稿与本组现有行做行级 diff,键匹配且字段有变则 update(保留原 id),草稿多出的 insert,现有多出的 delete;只动本 groupId 的行,同表其他课程不受影响(AddCourseScreen.kt:357-363、data/diff/RowKeyDiffer.kt:21-73、data/repository/ScheduleRepository.kt:281-289)。新建模式:全部草稿共享一个新生成的 UUID groupId,一次插入(AddCourseScreen.kt:364-367)。
  6. 撤回批。本次保存带新建槽位或槽位时间改动时,课程行写入与 timeJson 写回包在 UndoManager.beginBatch() / endBatch() 之间,撤回一次整步回退到保存前,课程和槽位一起还原(AddCourseScreen.kt:369-387、data/undo/UndoManager.kt:43-50)。

删除

删除入口在编辑页内,保存按钮下方。点击弹确认框:「确定要删除课程「%1$s」吗?此操作不可撤销。」(AddCourseScreen.kt:672-709、res/values-zh-rCN/strings.xml:249)。确认后 deleteCourseGroup 删除同 groupId 的全部行,并回收不再被任何课程引用的边缘槽位(data/repository/ScheduleRepository.kt:230-238),随后退回课表页。

相关页面

源码:app/src/main/java/com/lingion/sleepy/ui/screen/edit/AddCourseScreen.kt · app/src/main/java/com/lingion/sleepy/util/TimeTableUtils.kt · app/src/main/java/com/lingion/sleepy/util/CourseDisplayUtil.kt · app/src/main/java/com/lingion/sleepy/util/WeekRangeOverlap.kt · app/src/main/java/com/lingion/sleepy/util/CourseColorUtil.kt · app/src/main/java/com/lingion/sleepy/data/diff/RowKey.kt · RowKeyDiffer.kt · app/src/main/java/com/lingion/sleepy/data/undo/UndoManager.kt · app/src/main/java/com/lingion/sleepy/data/repository/ScheduleRepository.kt · app/src/main/java/com/lingion/sleepy/MainActivity.kt

Clone this wiki locally