Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2e806e3
feat(agenda): add daily deadline cutoff window
PurpleRain-PR Nov 9, 2026
35c6aff
fix(agenda): shorten adjusted deadline label and test timetable badges
PurpleRain-PR Nov 9, 2026
c0eb179
feat(agenda): configure global deadline layout days
PurpleRain-PR Nov 10, 2026
45382d4
feat(settings): keep the native deadline clock resident
PurpleRain-PR Nov 10, 2026
150af82
feat(agenda): unify event and exam timeline styling
PurpleRain-PR Nov 10, 2026
90f0f69
feat(agenda): persist completion and one-shot exam auto completion
PurpleRain-PR Nov 10, 2026
ae2db6b
fix(academics): keep exam time filters independent of completion
PurpleRain-PR Nov 10, 2026
eac5320
fix(test): isolate completion diagnostics and update state expectations
PurpleRain-PR Nov 10, 2026
9248f2e
fix(test): keep completion diagnostics IO inside the test clock
PurpleRain-PR Nov 10, 2026
7b0cbc4
fix(test): declare diagnostics IO override final
PurpleRain-PR Nov 10, 2026
4acce20
refactor(agenda): inject completion error reporting for deterministic…
PurpleRain-PR Nov 10, 2026
8264b8d
merge: integrate main optimizations with deadline cutoff features
PurpleRain-PR Nov 11, 2026
0f13348
fix(agenda): initialize stable layout card span directly
PurpleRain-PR Nov 11, 2026
af486c3
fix(test): isolate merge cache updates and badge clock
PurpleRain-PR Nov 11, 2026
08ed417
docs(agenda): align merged architecture with feature semantics
PurpleRain-PR Nov 11, 2026
6c5ebe1
feat(agenda): make the event information bar optional and restyle it
PurpleRain-PR Nov 11, 2026
d769f89
feat(agenda): band event intervals and keep the hidden start line
PurpleRain-PR Nov 11, 2026
788caac
test(settings): scroll the lazy settings list before the info bar switch
PurpleRain-PR Nov 11, 2026
a421043
fix(agenda): compute band color before the cascade
PurpleRain-PR Nov 11, 2026
188d84f
test(agenda): compare band paint starts without DateTime <
PurpleRain-PR Nov 11, 2026
7280fb5
refactor(agenda): remove the unified exam/event completion model
PurpleRain-PR Nov 13, 2026
15a497e
fix(agenda): restore event cards and per-item deadline tails
dongdigua Nov 12, 2026
cd31518
feat(settings): add manual migration for default DDL tails
dongdigua Nov 12, 2026
de48ca7
refactor(agenda): share timeline captions and tail day bounds
dongdigua Nov 12, 2026
69527eb
Merge remote-tracking branch 'origin/main' into refactor/drop-unified…
PurpleRain-PR Nov 14, 2026
370b44f
fix(agenda): clear more-options headroom for floating labels
PurpleRain-PR Nov 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions android/app/src/main/kotlin/dev/so1ve/bitree/WidgetAgenda.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ internal data class WidgetContent(
val nextUpdate: Long,
)

internal fun readWidgetContent(context: Context): WidgetContent {
val now = Calendar.getInstance(campusZone)
internal fun readWidgetContent(
context: Context,
nowMillis: Long = System.currentTimeMillis(),
): WidgetContent {
val now = Calendar.getInstance(campusZone).apply { timeInMillis = nowMillis }
val today = dayStart(now.timeInMillis)
val tomorrow = (today.clone() as Calendar).apply { add(Calendar.DATE, 1) }
val evening = (today.clone() as Calendar).apply { set(Calendar.HOUR_OF_DAY, 21) }
Expand Down
52 changes: 52 additions & 0 deletions android/app/src/test/kotlin/dev/so1ve/bitree/WidgetAgendaTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.json.JSONArray
import org.json.JSONObject
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -89,6 +90,57 @@ class WidgetAgendaTest {
)
}

@Test
fun endedAgendaEntriesStayHiddenAndCoursesStillDriveNextUpdate() {
val now = timestamp("2026-11-09T12:00:00.000Z")
val courses = JSONArray()
.put(course("已下课", 1))
.put(course("下午课", 6))
val term = JSONObject()
.put("firstMonday", "2026-11-09")
.put("courses", courses)
File(context.filesDir, "schedule.json").writeText(
JSONObject().put("schemaVersion", 2).put("selectedTerm", "term")
.put("semesters", JSONObject().put("term", term)).toString(),
)
agendaFile.writeText(
JSONObject().put("schemaVersion", 2)
.put("entries", JSONArray().put(timed("已结束日程", "event")))
.toString(),
)

val content = readWidgetContent(context, now)

assertEquals(listOf("下午课"), content.entries.map { it.title })
assertEquals(timestamp("2026-11-09T14:05:00.000Z"), content.nextUpdate)
assertTrue(content.nextUpdate > now)
}

private fun timed(title: String, kind: String): JSONObject = JSONObject()
.put("id", title)
.put("title", title)
.put("kind", kind)
.put("startsAt", "2026-11-09T09:00:00.000Z")
.put("endsAt", "2026-11-09T11:00:00.000Z")
.put("allDay", false)
.put("location", "")
.put("done", false)
.put("deleted", false)

private fun course(name: String, section: Int): JSONObject = JSONObject()
.put("name", name)
.put("room", "")
.put("campus", "")
.put("weekday", 1)
.put("startSection", section)
.put("endSection", section)
.put("weeks", JSONArray().put(1))

private fun timestamp(value: String): Long =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
.apply { timeZone = TimeZone.getTimeZone("Asia/Shanghai") }
.parse(value)!!.time

private fun deadline(title: String): JSONObject {
val zone = TimeZone.getTimeZone("Asia/Shanghai")
val today = Calendar.getInstance(zone).apply {
Expand Down
10 changes: 5 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,22 @@ USTC 风控采集失败时遵循学校登录页的协议:仅在风控请求出

日程页提供列表与横向多泳道时间轴,共用类型筛选,读取同一个 `AgendaController`,不混入课表课程。列表保留未完成/已完成分页;时间轴将两种状态同表排布,不按完成状态分配泳道,已完成或已结束条目仍沿用弱化样式。视图模式通过 `SharedPreferencesAsync` 的 `agendaViewMode` 保存,缺省为瀑布图(时间轴),已有有效偏好仍按用户选择恢复;缩放和浏览位置不写入日程文件。切换视图保留本次浏览位置,离开日程页后再进入会将时间轴定位到今日 12:00 居中。

`agenda/timeline_layout.dart` 分别计算泳道占位和当前绘制区间。分行使用最小缩放时的卡片占位,缩放和平移不会重新分行;数据、窗口宽度或字体大小改变时才重新计算占位。DDL 正文保持 144 逻辑像素宽并随字体放大;普通日程和考试按真实时长绘制,最少显示固定 2 逻辑像素,过窄时通过点击、悬停或长按查看完整信息。`layoutEnd` 是当前显示终点,用于绘制、可见区域筛选和拖尾避让,不修改条目的真实时间。`tailDays` 默认为 4,可在 DDL 新建/编辑面板中使用滑动条在 1~30 天之间按整天调整,随条目保存;旧数据缺少该字段时使用 4 天。
`agenda/timeline_layout.dart` 将完整泳道预留与缩放绘制投影分开。普通 DDL 预留 `[有效截止-D天,真实截止]`,仅开始项目向右预留 D 天,普通日程和考试预留 `[真实开始-D天,真实结束]`;数据和基础布局参数不变时,缩放和平移不会重新分行。D 优先使用单条 DDL 的 `tailDays`;没有单独设置(`tailDays` 缺失或为 null)时跟随全局默认排版宽度,初始 4、范围 1~30 天。旧记录中已保存的单条长度保留优先级。编辑页“更多选项”提供拖尾滑条和“跟随全局设置”开关。普通日程和考试的信息区继续使用全局默认值。DDL 主体和日程左侧信息区按最小缩放 D−0.25 天对应的屏幕像素定宽,默认约 3.75 基线天,不采用 144px 独立正文或随缩放增长的日历天数拖尾。`paintStart/paintEnd` 表示当前实际绘制边界,真实时段和伪截止延伸段随缩放变化。纵向范围取全部筛选条目的最高泳道,不依赖横向可见条目。

`AgendaTimelineEntry` 只负责条目的外观和交互,不处理坐标或泳道。日程与考试在标题下常驻真实起止时间,跨天时分两行标出日期;全天日程显示日期范围。DDL 保留课程/来源,并另列截止时间。长按或鼠标悬停显示包含开始、结束或截止日期、课程及地点的分行预览;点按沿用详情抽屉。卡片最小高度为 56 逻辑像素,并随字体放大,确保完整时间与来源信息有独立空间。

DDL 的右端为 `dueAt`,拖尾从左侧渐隐;只有开放时间的测验以开放时间为左端,拖尾向右延伸。拖尾按 `tailDays - 0.25` 个日历日计算,随时间轴缩放,并在同泳道相邻卡片处截短;整条宽度至少容纳正文。渐变区域不拦截点击,短日程的扩展点击区域也不会越过下一张卡片。全天 DDL 的右端沿用模型定义,为对应日期次日零点。
DDL 主体右端为有效 `dueAt`,从左侧渐隐;仅有开放时间的项目从左向右淡出。主体屏幕定宽,不因邻卡裁短信息区。每日伪提前窗口支持关闭、跨日、两端包含及起止同点;原始截止不写回覆盖。命中窗口时,有效与真实截止之间绘制随时间缩放的宽斜纹矩形,真实标线使用变淡色并始终最后绘制,包括重叠情形。普通日程和考试真实时段为两条实线之间 45° 双色宽斜纹填充(实填充色与高亮色两带全不透明交替,首带为实色),左信息区使用实填充色与其半透明交替的斜纹及向内半线宽高亮色边框;设置中的“日程与考试信息栏”开关(默认开启)关闭时改用旧版方框卡片,在真实时段内显示标题与时间,并取消左侧信息区预留,占位改为 `[真实开始, 真实结束]`;DDL、仅开始项目不受影响。各区域完成透明度都乘原 alpha,已半透明区域再衰减,详见 [布局与伪截止规则](deadline-cutoff.md)。全天 DDL 的真实截止仍为对应日期次日零点,只有有效截止可派生提前。

时间轴以视口中心时间和连续缩放比例计算坐标,不创建覆盖历史与未来的巨大滚动画布。默认一天一格,最多一小时一格;基准格宽为 48~84 逻辑像素。平移没有预设日期边界,只绘制可见区域附近的网格和条目。顶部工具栏只保留单行并排的「今天」按钮与自适应宽度的缩放滑条,不显示当前日期和常驻的「时间/格」文本,缩放比例仅保留无障碍语义描述。表头基准高度为 24 逻辑像素:不显示小时时,第一行显示日期、第二行居中显示星期;显示小时时,第一行将「日期 星期」作为一个整体居中,第二行显示小时。标签宽度测量包含完整星期与中间空格,固定在对应时间格的中上方,以完整文字绘制并在视口边缘自然裁切,不贴靠屏幕边缘或变成省略号。首个日程条距表头分隔线 12 逻辑像素,与日程条之间的轨道间距一致。工具栏不再包含问号帮助图标及其提示。当前时间线单独在前景绘制,覆盖日程条但不拦截交互,也不绘制圆点。条目单击复用日程详情弹层,编辑、删除、同步后由功能控制器通知刷新。

时间轴使用独立的语义色,不复用楼栋调色板。考试、大物实验 DDL、乐学 DDL、新延河课堂 DDL 和 MOOC DDL 分别使用黄、蓝、橙、紫和薄荷绿,且每种颜色直接定义浅色与深色的 `fill`、`text`、`accent`。背景使用 fill、文字使用 text、DDL 右侧条与其他日程边框使用 accent。未完成且逾期的 DDL 沿用普通 DDL 的左淡出卡片样式,将 fill 替换为 errorContainer、文字替换为 onErrorContainer、强调色替换为 error;背景右侧再以 20% onErrorContainer 绘制大号警告图标虚影,来源色和错误色不进行长距离混色。末尾直接显示截止时间与“已逾期”。完成该条目后移除红色状态与虚影,并恢复来源色的淡化样式。已完成 DDL 和已结束考试的背景、文字、图标及边框/右侧条统一使用 50% 不透明度(50% 透明),保留各自原色;普通活动的完成样式不变。列表和课表不使用这套映射。
时间轴使用独立的语义色,不复用楼栋调色板。考试、大物实验 DDL、乐学 DDL、新延河课堂 DDL 和 MOOC DDL 分别使用黄、蓝、橙、紫和薄荷绿,且每种颜色直接定义浅色与深色的 `fill`、`text`、`accent`。背景使用 fill、文字使用 text、DDL 右侧条与其他日程边框使用 accent。未完成且逾期的 DDL 沿用普通 DDL 的左淡出卡片样式,将 fill 替换为 errorContainer、文字替换为 onErrorContainer、强调色替换为 error;背景右侧再以 20% onErrorContainer 绘制大号警告图标虚影,来源色和错误色不进行长距离混色。末尾直接显示截止时间与“已逾期”。完成该条目后移除红色状态与虚影,并恢复来源色的淡化样式。所有已完成或已结束项目的背景、文字、图标及标线统一在原透明度上乘 0.5;左信息区条纹、伪截止延伸和真实标线各自的半透明系数再与完成系数相乘。日程自定义颜色优先,仍保留其原 alpha,不以固定 0.5 覆盖。列表和课表不使用这套映射。

主页课表网格中的日程块不复用时间轴的语义来源色:普通活动使用 `primaryContainer` 背景、`onPrimaryContainer` 文字和 1px `primary` 边框,考试使用 `errorContainer`/`onErrorContainer`/`error`。浅色模式下容器色与页面底色天然接近,可见性由 tone 固定的边框和文字承担,因此开启全局系统调色板后仍能辨认;不使用 `outlineVariant` 等低对比细分隔色。

关闭全局系统调色板时,所选 JSON 调色板只覆盖课程与普通日程块,不修改页面背景、全局 `ColorScheme` 或应用明暗模式。内置 Mocha/Macchiato/Frappé 位于 `assets/palettes/`,颜色来自 [Catppuccin palette](https://github.com/catppuccin/palette/blob/main/palette.json),均适用于深色模式。课程使用所属区域的强调色,普通日程使用 `agenda`;最终底色统一为 `Color.lerp(base, accent, 0.24)`,文字统一使用 `text`。主页普通日程块与时间轴普通活动、无平台来源的手动 DDL 使用同一组颜色。时间轴中的平台作业、MOOC、物理实验与考试继续保留来源映射;未完成且逾期的 DDL 使用轻量错误色蒙版,主页考试沿用错误色,完成淡化规则不变。系统调色板可用且已启用时优先于 JSON 配色。
设置的「外观」二级页面提供明暗模式切换、浅色/深色调色板独立选择、JSON 文件导入、自定义配色的删除按钮及系统调色板开关,所有选项均可直接使用。关于弹窗中的五次连击仍可开启本次运行的开发者模式,但目前不影响任何功能;解锁后弹窗保持打开并在版本号下方提示,避免多余的连击误触后面的设置项。导入成功后将完整配色内容复制到应用设置并应用到对应模式,不依赖原文件继续存在;校验或保存失败时保留原选择。删除正在使用的自定义配色会将对应模式恢复为默认。旧版本的 Cat 配色选择会自动恢复。Android 小组件接收浅色和深色课程的最终颜色,其系统课程配色开关仍独立控制;Android 12+ 的组件整体背景与文字继续使用系统颜色。文件格式与字段说明见 [调色板文件](palettes.md)。
设置的「外观」二级页面提供明暗模式切换、浅色/深色调色板独立选择、JSON 文件导入、自定义配色的删除按钮及系统调色板开关,所有选项均可直接使用。关于弹窗中的五次连击可开启本次运行的开发者模式,设置页随后显示“开发者选项”;其中“将默认拖尾长度设为 null”按钮会将所有 4 天的 DDL 拖尾(含回收站)一次性改为跟随全局设置,其它长度保留,不自动迁移。解锁后弹窗保持打开并在版本号下方提示,避免多余的连击误触后面的设置项。导入成功后将完整配色内容复制到应用设置并应用到对应模式,不依赖原文件继续存在;校验或保存失败时保留原选择。删除正在使用的自定义配色会将对应模式恢复为默认。旧版本的 Cat 配色选择会自动恢复。Android 小组件接收浅色和深色课程的最终颜色,其系统课程配色开关仍独立控制;Android 12+ 的组件整体背景与文字继续使用系统颜色。文件格式与字段说明见 [调色板文件](palettes.md)。

操作方式:拖动或普通滚轮横向浏览时间,双指捏合、`Ctrl + 滚轮` 或滑条连续缩放;上下拖动或 `Shift + 滚轮` 浏览泳道。时间轴隐藏显式与桌面自动添加的纵向滚动条,仍保留纵向滚动和惯性。单指或鼠标拖动释放后,使用 Flutter 的摩擦物理模拟继续滑行;缩放、新输入、隐藏视图或离开页面时停止惯性,触控板自身的惯性输入不额外叠加甩动。「今天」按钮回到今日居中。日程列表的完成状态以分段选择过滤,不使用横向分页;时间轴的横拖只用于时间导航。
操作方式:拖动或普通滚轮横向浏览时间,双指捏合、`Ctrl + 滚轮` 或滑条连续缩放;上下拖动或 `Shift + 滚轮` 浏览泳道。时间轴隐藏显式与桌面自动添加的纵向滚动条,仍保留纵向滚动和惯性。纵向拖动通过 `ScrollPosition.drag/update/end` 使用 Flutter 的惯性和回弹,横向中心日期使用摩擦模拟;正常 overscroll 保留,筛选或尺寸缩小导致的旧偏移与卡片裁剪同步纠正。平移中心日期通过局部监听更新视口,网格文字和卡片内容层复用缓存,动态几何独立更新;缩放、新输入、隐藏视图或离开页面时停止惯性,触控板自身的惯性输入不额外叠加甩动。「今天」按钮回到今日居中。日程列表的完成状态以分段选择过滤,不使用横向分页;时间轴的横拖只用于时间导航。

## 中国大学MOOC

Expand Down
Loading
Loading