-
Notifications
You must be signed in to change notification settings - Fork 8
Add Course
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 拼接)。
解析失败(时间非法 / 节次不存在)返回空,由校验报错,不静默给值。
点「新建」候选,弹出对话框填起止时间,两个时间都能解析才可确认(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)。点保存走六步:
-
校验。课名非空、整课周次为正;每卡:星期至少一天、起始周 ≤ 结束周;非常规节次卡的槽位必须真实存在,标准卡节次不得越过 maxStd;勾了非常规时间的卡检查时间格式与先后。最后做卡间两两重叠检测:同星期、生效时间区间相交(按分钟折算)、且周类型在公共周内同时命中,才算撞(
AddCourseScreen.kt:816-893、util/WeekRangeOverlap.kt:5-22)。失败:顶部红卡列出问题(最多显 4 条,余下折叠计数),对应卡红底加卡内红字,不落库(AddCourseScreen.kt:500-504、AddCourseScreen.kt:943-973、AddCourseScreen.kt:1223-1233)。 -
无表自动建表。当前没有课表时先 createEmptyTable() 建一张再继续(
AddCourseScreen.kt:330-333、ui/screen/schedule/ScheduleViewModel.kt:171-183)。 -
ownTime 反算真实节点。勾了非常规时间的卡(非边缘槽位卡)落库前用
timeToNode把覆盖起止映射回真实节次区间,今日页、桌面组件这类只认节点的消费方拿不到占位节点;映射失败保原值(AddCourseScreen.kt:785-791、util/TimeTableUtils.kt:106-139)。 -
非阻塞冲突检查。与本表其他课程比对,编辑模式排除本组旧记录(自己不算撞自己),命中弹明细:星期、节次、周次、撞哪门课,最多显 6 条(
AddCourseScreen.kt:338-356、AddCourseScreen.kt:394-424、strings.xml:266-268)。「仍然保存」跳过检查直接落库;「返回修改」放弃本次。明细列表用 rememberSaveable 持有,屏幕旋转后弹窗还在(AddCourseScreen.kt:237-239)。 -
落库。编辑模式: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)。 -
撤回批。本次保存带新建槽位或槽位时间改动时,课程行写入与 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
Sleepy Wiki
入门
界面与视图
课程管理
导入导出
小组件与提醒
数据与设置
项目
社区