Codex commited on
Commit
3b5b6d8
·
1 Parent(s): 4d1cba1

Fix planner axis and import class schedule

Browse files
Files changed (3) hide show
  1. static/planner.js +46 -49
  2. static/v020.css +52 -18
  3. storage.py +227 -1
static/planner.js CHANGED
@@ -10,10 +10,10 @@
10
  suppressClickUntil: 0,
11
  };
12
 
13
- const PIXELS_PER_MINUTE = 1.16;
14
- const AXIS_WIDTH = 96;
15
- const SLOT_WIDTH = 156;
16
- const CANVAS_GAP = 18;
17
  const SNAP_MINUTES = 5;
18
  const MIN_DURATION = 15;
19
  const MIN_BLOCK_HEIGHT = MIN_DURATION * PIXELS_PER_MINUTE;
@@ -162,7 +162,7 @@
162
  }
163
 
164
  function getPlannerHeight() {
165
- return getPlannerConfig().totalMinutes * PIXELS_PER_MINUTE;
166
  }
167
 
168
  function getCanvasRect() {
@@ -196,13 +196,17 @@
196
  return minutes * PIXELS_PER_MINUTE;
197
  }
198
 
 
 
 
 
199
  function getBlockHeight(startMinutes, endMinutes) {
200
- return Math.max(minutesToPixels(endMinutes - startMinutes), MIN_BLOCK_HEIGHT);
201
  }
202
 
203
  function setBlockBounds(block, startMinutes, endMinutes, dayStart) {
204
  const height = getBlockHeight(startMinutes, endMinutes);
205
- block.style.top = `${minutesToPixels(startMinutes - dayStart)}px`;
206
  block.style.height = `${height}px`;
207
  return height;
208
  }
@@ -218,6 +222,18 @@
218
  return snapMinutes((clientY - pointerStartY) / PIXELS_PER_MINUTE);
219
  }
220
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  function suppressRecentClicks(duration = CLICK_SUPPRESS_MS) {
222
  state.suppressClickUntil = Date.now() + duration;
223
  }
@@ -334,7 +350,7 @@
334
  line.style.display = "block";
335
  line.style.left = `${canvasLeft}px`;
336
  line.style.right = "14px";
337
- line.style.top = `${(currentMinutes - dayStart) * PIXELS_PER_MINUTE}px`;
338
  }
339
 
340
  function renderTaskPool() {
@@ -395,65 +411,46 @@
395
  nowLine.className = "timeline-now-line";
396
  nowLine.id = "timelineNowLine";
397
 
 
 
 
 
398
  const lineMarkers = new Set([dayStart, dayEnd]);
399
- const preferredAxisCandidates = new Set([dayStart, dayEnd]);
400
- const hourAxisCandidates = new Set();
401
 
402
- (state.planner.time_slots || []).forEach((slot) => {
403
  const slotStart = toMinutes(slot.start);
404
  const slotEnd = toMinutes(slot.end);
405
  lineMarkers.add(slotStart);
406
  lineMarkers.add(slotEnd);
407
- preferredAxisCandidates.add(slotStart);
408
 
409
  const band = document.createElement("div");
410
  band.className = "timeline-slot-band";
411
- band.style.top = `${minutesToPixels(slotStart - dayStart)}px`;
412
- band.style.height = `${minutesToPixels(slotEnd - slotStart)}px`;
413
  band.innerHTML = `
414
- <strong>${escapeHtml(slot.label)}</strong>
415
  <span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
416
  `;
417
  slotLayer.appendChild(band);
418
  });
419
 
420
- for (let minute = Math.ceil(dayStart / 60) * 60; minute <= dayEnd; minute += 60) {
421
- lineMarkers.add(minute);
422
- hourAxisCandidates.add(minute);
423
- }
424
-
425
  Array.from(lineMarkers).sort((left, right) => left - right).forEach((minute) => {
426
  const line = document.createElement("div");
427
- line.className = `timeline-line ${minute % 60 === 0 ? "is-hour" : "is-slot"}`;
428
- line.style.top = `${minutesToPixels(minute - dayStart)}px`;
429
  canvasLayer.appendChild(line);
430
  });
431
 
432
- const axisLabelMinutes = [];
433
- const appendAxisLabel = (minute) => {
434
- const top = minutesToPixels(minute - dayStart);
435
- const previous = axisLabelMinutes[axisLabelMinutes.length - 1];
436
- if (previous !== undefined) {
437
- const previousTop = minutesToPixels(previous - dayStart);
438
- if (top - previousTop < AXIS_LABEL_MIN_GAP) {
439
- return;
440
- }
441
- }
442
- axisLabelMinutes.push(minute);
443
- };
444
-
445
- Array.from(preferredAxisCandidates)
446
- .sort((left, right) => left - right)
447
- .forEach(appendAxisLabel);
448
-
449
- Array.from(hourAxisCandidates)
450
- .sort((left, right) => left - right)
451
- .forEach(appendAxisLabel);
452
-
453
- axisLabelMinutes.forEach((minute) => {
454
  const tick = document.createElement("div");
455
  tick.className = "timeline-axis-tick";
456
- tick.style.top = `${minutesToPixels(minute - dayStart)}px`;
 
 
 
 
 
457
  tick.textContent = minutesToTime(minute);
458
  axisLayer.appendChild(tick);
459
  });
@@ -465,14 +462,14 @@
465
  }
466
  });
467
 
468
- Array.from(majorMap.values()).forEach((block) => {
469
  const startMinutes = toMinutes(block.start);
470
  const endMinutes = toMinutes(block.end);
471
  const overlay = document.createElement("div");
472
  overlay.className = "timeline-major-block";
473
- overlay.style.top = `${minutesToPixels(startMinutes - dayStart)}px`;
474
- overlay.style.height = `${minutesToPixels(endMinutes - startMinutes)}px`;
475
- overlay.innerHTML = `<span>${escapeHtml(block.label)}</span>`;
476
  canvasLayer.appendChild(overlay);
477
  });
478
 
 
10
  suppressClickUntil: 0,
11
  };
12
 
13
+ const PIXELS_PER_MINUTE = 1.2;
14
+ const AXIS_WIDTH = 118;
15
+ const SLOT_WIDTH = 148;
16
+ const CANVAS_GAP = 16;
17
  const SNAP_MINUTES = 5;
18
  const MIN_DURATION = 15;
19
  const MIN_BLOCK_HEIGHT = MIN_DURATION * PIXELS_PER_MINUTE;
 
162
  }
163
 
164
  function getPlannerHeight() {
165
+ return timelinePixels(getPlannerConfig().totalMinutes);
166
  }
167
 
168
  function getCanvasRect() {
 
196
  return minutes * PIXELS_PER_MINUTE;
197
  }
198
 
199
+ function timelinePixels(minutes) {
200
+ return Math.round(minutesToPixels(minutes));
201
+ }
202
+
203
  function getBlockHeight(startMinutes, endMinutes) {
204
+ return Math.max(timelinePixels(endMinutes - startMinutes), Math.round(MIN_BLOCK_HEIGHT));
205
  }
206
 
207
  function setBlockBounds(block, startMinutes, endMinutes, dayStart) {
208
  const height = getBlockHeight(startMinutes, endMinutes);
209
+ block.style.top = `${timelinePixels(startMinutes - dayStart)}px`;
210
  block.style.height = `${height}px`;
211
  return height;
212
  }
 
222
  return snapMinutes((clientY - pointerStartY) / PIXELS_PER_MINUTE);
223
  }
224
 
225
+ function formatLessonLabel(index) {
226
+ return `第${String(index + 1).padStart(2, "0")}节`;
227
+ }
228
+
229
+ function getTimelineAxisMinutes() {
230
+ const axisMinutes = new Set([getPlannerConfig().dayStart, getPlannerConfig().dayEnd]);
231
+ (state.planner.time_slots || []).forEach((slot) => {
232
+ axisMinutes.add(toMinutes(slot.start));
233
+ });
234
+ return Array.from(axisMinutes).sort((left, right) => left - right);
235
+ }
236
+
237
  function suppressRecentClicks(duration = CLICK_SUPPRESS_MS) {
238
  state.suppressClickUntil = Date.now() + duration;
239
  }
 
350
  line.style.display = "block";
351
  line.style.left = `${canvasLeft}px`;
352
  line.style.right = "14px";
353
+ line.style.top = `${timelinePixels(currentMinutes - dayStart)}px`;
354
  }
355
 
356
  function renderTaskPool() {
 
411
  nowLine.className = "timeline-now-line";
412
  nowLine.id = "timelineNowLine";
413
 
414
+ const axisRail = document.createElement("div");
415
+ axisRail.className = "timeline-axis-rail";
416
+ axisLayer.appendChild(axisRail);
417
+
418
  const lineMarkers = new Set([dayStart, dayEnd]);
419
+ const timeSlots = state.planner.time_slots || [];
 
420
 
421
+ timeSlots.forEach((slot, slotIndex) => {
422
  const slotStart = toMinutes(slot.start);
423
  const slotEnd = toMinutes(slot.end);
424
  lineMarkers.add(slotStart);
425
  lineMarkers.add(slotEnd);
 
426
 
427
  const band = document.createElement("div");
428
  band.className = "timeline-slot-band";
429
+ band.style.top = `${timelinePixels(slotStart - dayStart)}px`;
430
+ band.style.height = `${timelinePixels(slotEnd - slotStart)}px`;
431
  band.innerHTML = `
432
+ <strong>${formatLessonLabel(slotIndex)}</strong>
433
  <span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
434
  `;
435
  slotLayer.appendChild(band);
436
  });
437
 
 
 
 
 
 
438
  Array.from(lineMarkers).sort((left, right) => left - right).forEach((minute) => {
439
  const line = document.createElement("div");
440
+ line.className = "timeline-line is-slot";
441
+ line.style.top = `${timelinePixels(minute - dayStart)}px`;
442
  canvasLayer.appendChild(line);
443
  });
444
 
445
+ getTimelineAxisMinutes().forEach((minute, axisIndex, axisMinutes) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  const tick = document.createElement("div");
447
  tick.className = "timeline-axis-tick";
448
+ if (axisIndex === 0) {
449
+ tick.classList.add("is-leading");
450
+ } else if (axisIndex === axisMinutes.length - 1) {
451
+ tick.classList.add("is-terminal");
452
+ }
453
+ tick.style.top = `${timelinePixels(minute - dayStart)}px`;
454
  tick.textContent = minutesToTime(minute);
455
  axisLayer.appendChild(tick);
456
  });
 
462
  }
463
  });
464
 
465
+ Array.from(majorMap.values()).forEach((block, blockIndex) => {
466
  const startMinutes = toMinutes(block.start);
467
  const endMinutes = toMinutes(block.end);
468
  const overlay = document.createElement("div");
469
  overlay.className = "timeline-major-block";
470
+ overlay.style.top = `${timelinePixels(startMinutes - dayStart)}px`;
471
+ overlay.style.height = `${timelinePixels(endMinutes - startMinutes)}px`;
472
+ overlay.innerHTML = `<span>${escapeHtml(block.label || `第${blockIndex + 1}大节`)}</span>`;
473
  canvasLayer.appendChild(overlay);
474
  });
475
 
static/v020.css CHANGED
@@ -218,40 +218,78 @@ body.planner-interacting * {
218
  right: 12px;
219
  }
220
 
 
 
 
 
 
 
 
 
 
221
  .timeline-axis-tick {
222
  position: absolute;
223
  left: 0;
224
- right: 10px;
225
  display: flex;
226
  justify-content: flex-end;
 
 
227
  transform: translateY(-50%);
228
- padding-right: 12px;
229
- color: rgba(238, 244, 251, 0.7);
230
- font-size: 0.82rem;
 
 
231
  line-height: 1;
232
  white-space: nowrap;
233
  }
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  .timeline-slot-band {
236
  position: absolute;
237
- left: 10px;
238
  right: 10px;
239
  box-sizing: border-box;
240
- padding: 12px;
241
- border-radius: 18px;
242
- background: rgba(255, 255, 255, 0.04);
243
- border: 1px solid rgba(255, 255, 255, 0.06);
244
- display: grid;
245
- gap: 6px;
 
 
 
 
246
  }
247
 
248
  .timeline-slot-band strong {
249
- font-size: 0.96rem;
 
250
  }
251
 
252
  .timeline-slot-band span {
253
  color: var(--muted);
254
- font-size: 0.82rem;
 
 
255
  }
256
 
257
  .timeline-line {
@@ -261,12 +299,8 @@ body.planner-interacting * {
261
  height: 1px;
262
  }
263
 
264
- .timeline-line.is-hour {
265
- background: rgba(255, 255, 255, 0.11);
266
- }
267
-
268
  .timeline-line.is-slot {
269
- background: rgba(255, 255, 255, 0.06);
270
  }
271
 
272
  .timeline-major-block {
 
218
  right: 12px;
219
  }
220
 
221
+ .timeline-axis-rail {
222
+ position: absolute;
223
+ top: 0;
224
+ bottom: 0;
225
+ right: 14px;
226
+ width: 1px;
227
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(123, 231, 234, 0.18), rgba(255, 255, 255, 0.06));
228
+ }
229
+
230
  .timeline-axis-tick {
231
  position: absolute;
232
  left: 0;
233
+ right: 0;
234
  display: flex;
235
  justify-content: flex-end;
236
+ align-items: center;
237
+ gap: 12px;
238
  transform: translateY(-50%);
239
+ padding-right: 26px;
240
+ color: rgba(238, 244, 251, 0.76);
241
+ font-size: 0.8rem;
242
+ font-variant-numeric: tabular-nums;
243
+ letter-spacing: 0.02em;
244
  line-height: 1;
245
  white-space: nowrap;
246
  }
247
 
248
+ .timeline-axis-tick::after {
249
+ content: "";
250
+ width: 8px;
251
+ height: 8px;
252
+ border-radius: 999px;
253
+ background: rgba(123, 231, 234, 0.78);
254
+ box-shadow: 0 0 0 4px rgba(123, 231, 234, 0.12);
255
+ flex: none;
256
+ }
257
+
258
+ .timeline-axis-tick.is-leading {
259
+ transform: translateY(0);
260
+ }
261
+
262
+ .timeline-axis-tick.is-terminal {
263
+ transform: translateY(-100%);
264
+ }
265
+
266
  .timeline-slot-band {
267
  position: absolute;
268
+ left: 8px;
269
  right: 10px;
270
  box-sizing: border-box;
271
+ overflow: hidden;
272
+ padding: 8px 10px;
273
+ border-radius: 16px;
274
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.025));
275
+ border: 1px solid rgba(255, 255, 255, 0.08);
276
+ display: flex;
277
+ flex-direction: column;
278
+ justify-content: center;
279
+ gap: 4px;
280
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
281
  }
282
 
283
  .timeline-slot-band strong {
284
+ font-size: 0.86rem;
285
+ line-height: 1.1;
286
  }
287
 
288
  .timeline-slot-band span {
289
  color: var(--muted);
290
+ font-size: 0.74rem;
291
+ line-height: 1.1;
292
+ font-variant-numeric: tabular-nums;
293
  }
294
 
295
  .timeline-line {
 
299
  height: 1px;
300
  }
301
 
 
 
 
 
302
  .timeline-line.is-slot {
303
+ background: rgba(255, 255, 255, 0.08);
304
  }
305
 
306
  .timeline-major-block {
storage.py CHANGED
@@ -22,6 +22,199 @@ COURSE_COLOR_PALETTE = [
22
  "#b197fc",
23
  "#83c5be",
24
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
  def beijing_now() -> datetime:
@@ -36,6 +229,30 @@ def make_id(prefix: str) -> str:
36
  return f"{prefix}_{uuid.uuid4().hex[:10]}"
37
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  class ReminderStore:
40
  def __init__(self, path: Path):
41
  self.path = path
@@ -90,7 +307,8 @@ class ReminderStore:
90
  ],
91
  },
92
  ],
93
- "courses": [],
 
94
  "schedule_settings": deepcopy(DEFAULT_SCHEDULE_SETTINGS),
95
  }
96
 
@@ -100,12 +318,20 @@ class ReminderStore:
100
  categories = data.setdefault("categories", [])
101
  courses = data.setdefault("courses", [])
102
  settings = data.setdefault("schedule_settings", {})
 
103
 
104
  for key, value in DEFAULT_SCHEDULE_SETTINGS.items():
105
  if key not in settings:
106
  settings[key] = value
107
  changed = True
108
 
 
 
 
 
 
 
 
109
  for category in categories:
110
  if "created_at" not in category:
111
  category["created_at"] = iso_now()
 
22
  "#b197fc",
23
  "#83c5be",
24
  ]
25
+ DEFAULT_COURSE_SEED_VERSION = "2026-spring-v1"
26
+ DEFAULT_PERIOD_TIMES = {
27
+ 1: ("08:15", "09:00"),
28
+ 2: ("09:10", "09:55"),
29
+ 3: ("10:15", "11:00"),
30
+ 4: ("11:10", "11:55"),
31
+ 5: ("13:50", "14:35"),
32
+ 6: ("14:45", "15:30"),
33
+ 7: ("15:40", "16:25"),
34
+ 8: ("16:45", "17:30"),
35
+ 9: ("17:40", "18:25"),
36
+ 10: ("19:20", "20:05"),
37
+ 11: ("20:15", "21:00"),
38
+ 12: ("21:10", "21:55"),
39
+ }
40
+ DEFAULT_IMPORTED_COURSES = [
41
+ {
42
+ "title": "形势与政策-2_20",
43
+ "day_of_week": 7,
44
+ "start_period": 10,
45
+ "end_period": 11,
46
+ "start_week": 9,
47
+ "end_week": 15,
48
+ "week_pattern": "odd",
49
+ "location": "江安综合楼C座C407",
50
+ "color": "#E0B100",
51
+ },
52
+ {
53
+ "title": "数字逻辑:应用与设计_09",
54
+ "day_of_week": 1,
55
+ "start_period": 5,
56
+ "end_period": 7,
57
+ "start_week": 1,
58
+ "end_week": 16,
59
+ "week_pattern": "all",
60
+ "location": "江安一教A座A412",
61
+ "color": "#FF5AA5",
62
+ },
63
+ {
64
+ "title": "中国近现代史纲要_59",
65
+ "day_of_week": 1,
66
+ "start_period": 10,
67
+ "end_period": 12,
68
+ "start_week": 1,
69
+ "end_week": 16,
70
+ "week_pattern": "all",
71
+ "location": "江安综合楼C座C403",
72
+ "color": "#66BB6A",
73
+ },
74
+ {
75
+ "title": "人工智能导论_666",
76
+ "day_of_week": 2,
77
+ "start_period": 1,
78
+ "end_period": 2,
79
+ "start_week": 1,
80
+ "end_week": 16,
81
+ "week_pattern": "all",
82
+ "location": "江安一教B座B201",
83
+ "color": "#C77400",
84
+ },
85
+ {
86
+ "title": "微积分(I)-2_33",
87
+ "day_of_week": 2,
88
+ "start_period": 3,
89
+ "end_period": 4,
90
+ "start_week": 1,
91
+ "end_week": 16,
92
+ "week_pattern": "all",
93
+ "location": "江安一教B座B101",
94
+ "color": "#DD8E88",
95
+ },
96
+ {
97
+ "title": "体育-2游泳_12",
98
+ "day_of_week": 2,
99
+ "start_period": 5,
100
+ "end_period": 6,
101
+ "start_week": 1,
102
+ "end_week": 12,
103
+ "week_pattern": "all",
104
+ "location": "江安未来游泳馆",
105
+ "color": "#717171",
106
+ },
107
+ {
108
+ "title": "城市经济学_03",
109
+ "day_of_week": 2,
110
+ "start_period": 8,
111
+ "end_period": 9,
112
+ "start_week": 1,
113
+ "end_week": 16,
114
+ "week_pattern": "all",
115
+ "location": "江安一教A座A308",
116
+ "color": "#F6AD9A",
117
+ },
118
+ {
119
+ "title": "新中国史_02",
120
+ "day_of_week": 2,
121
+ "start_period": 10,
122
+ "end_period": 12,
123
+ "start_week": 1,
124
+ "end_week": 11,
125
+ "week_pattern": "all",
126
+ "location": "江安综合楼C座C407",
127
+ "color": "#FF9A6A",
128
+ },
129
+ {
130
+ "title": "通用英语 I-2_49",
131
+ "day_of_week": 3,
132
+ "start_period": 1,
133
+ "end_period": 2,
134
+ "start_week": 1,
135
+ "end_week": 16,
136
+ "week_pattern": "all",
137
+ "location": "江安二基楼B座B409",
138
+ "color": "#55C08D",
139
+ },
140
+ {
141
+ "title": "线性代数(理工)_35",
142
+ "day_of_week": 3,
143
+ "start_period": 3,
144
+ "end_period": 4,
145
+ "start_week": 1,
146
+ "end_week": 16,
147
+ "week_pattern": "all",
148
+ "location": "江安综合楼C座C303",
149
+ "color": "#9A6EAB",
150
+ },
151
+ {
152
+ "title": "微积分(I)-2_33",
153
+ "day_of_week": 4,
154
+ "start_period": 1,
155
+ "end_period": 3,
156
+ "start_week": 1,
157
+ "end_week": 16,
158
+ "week_pattern": "all",
159
+ "location": "江安一教B座B101",
160
+ "color": "#DD8E88",
161
+ },
162
+ {
163
+ "title": "面向对象程序设计(Java篇)_03",
164
+ "day_of_week": 4,
165
+ "start_period": 5,
166
+ "end_period": 8,
167
+ "start_week": 1,
168
+ "end_week": 13,
169
+ "week_pattern": "all",
170
+ "location": "江安综合楼B座B205",
171
+ "color": "#C99B89",
172
+ },
173
+ {
174
+ "title": "深度学习_01",
175
+ "day_of_week": 4,
176
+ "start_period": 10,
177
+ "end_period": 12,
178
+ "start_week": 6,
179
+ "end_week": 16,
180
+ "week_pattern": "all",
181
+ "location": "江安一教A座A207",
182
+ "color": "#FA8D92",
183
+ },
184
+ {
185
+ "title": "大学物理(理工)III-1_09",
186
+ "day_of_week": 5,
187
+ "start_period": 1,
188
+ "end_period": 2,
189
+ "start_week": 1,
190
+ "end_week": 16,
191
+ "week_pattern": "all",
192
+ "location": "江安一教B座B401",
193
+ "color": "#8A860C",
194
+ },
195
+ {
196
+ "title": "线性代数(理工)_35",
197
+ "day_of_week": 5,
198
+ "start_period": 3,
199
+ "end_period": 4,
200
+ "start_week": 1,
201
+ "end_week": 16,
202
+ "week_pattern": "all",
203
+ "location": "江安一教B座B301",
204
+ "color": "#9A6EAB",
205
+ },
206
+ {
207
+ "title": "线性代数习题课_35",
208
+ "day_of_week": 6,
209
+ "start_period": 7,
210
+ "end_period": 8,
211
+ "start_week": 2,
212
+ "end_week": 16,
213
+ "week_pattern": "all",
214
+ "location": "江安一教B座B104",
215
+ "color": "#F0A794",
216
+ },
217
+ ]
218
 
219
 
220
  def beijing_now() -> datetime:
 
229
  return f"{prefix}_{uuid.uuid4().hex[:10]}"
230
 
231
 
232
+ def build_default_courses() -> list[dict[str, Any]]:
233
+ created_at = iso_now()
234
+ courses: list[dict[str, Any]] = []
235
+ for index, item in enumerate(DEFAULT_IMPORTED_COURSES):
236
+ start_time = DEFAULT_PERIOD_TIMES[item["start_period"]][0]
237
+ end_time = DEFAULT_PERIOD_TIMES[item["end_period"]][1]
238
+ courses.append(
239
+ {
240
+ "id": make_id("course"),
241
+ "title": item["title"],
242
+ "day_of_week": item["day_of_week"],
243
+ "start_time": start_time,
244
+ "end_time": end_time,
245
+ "start_week": item["start_week"],
246
+ "end_week": item["end_week"],
247
+ "week_pattern": item["week_pattern"],
248
+ "location": item["location"],
249
+ "color": item.get("color") or COURSE_COLOR_PALETTE[index % len(COURSE_COLOR_PALETTE)],
250
+ "created_at": created_at,
251
+ }
252
+ )
253
+ return courses
254
+
255
+
256
  class ReminderStore:
257
  def __init__(self, path: Path):
258
  self.path = path
 
307
  ],
308
  },
309
  ],
310
+ "courses": build_default_courses(),
311
+ "course_seed_version": DEFAULT_COURSE_SEED_VERSION,
312
  "schedule_settings": deepcopy(DEFAULT_SCHEDULE_SETTINGS),
313
  }
314
 
 
318
  categories = data.setdefault("categories", [])
319
  courses = data.setdefault("courses", [])
320
  settings = data.setdefault("schedule_settings", {})
321
+ course_seed_version = data.get("course_seed_version")
322
 
323
  for key, value in DEFAULT_SCHEDULE_SETTINGS.items():
324
  if key not in settings:
325
  settings[key] = value
326
  changed = True
327
 
328
+ if course_seed_version != DEFAULT_COURSE_SEED_VERSION:
329
+ if not courses:
330
+ data["courses"] = build_default_courses()
331
+ courses = data["courses"]
332
+ data["course_seed_version"] = DEFAULT_COURSE_SEED_VERSION
333
+ changed = True
334
+
335
  for category in categories:
336
  if "created_at" not in category:
337
  category["created_at"] = iso_now()