@nbtca/prompt 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +167 -64
- package/dist/app/chrome.js +67 -50
- package/dist/app/fields/list-field.js +12 -25
- package/dist/app/fields/text-field.js +3 -8
- package/dist/app/frame.js +16 -23
- package/dist/app/keys.js +110 -2
- package/dist/app/views/docs-render.js +34 -26
- package/dist/app/views/docs.js +280 -68
- package/dist/app/views/events-render.js +21 -27
- package/dist/app/views/events.js +57 -33
- package/dist/app/views/home.js +67 -47
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +51 -74
- package/dist/app/views/schedule.js +246 -101
- package/dist/app/views/settings-render.js +8 -19
- package/dist/app/views/settings.js +92 -17
- package/dist/auth/cookie-transport.js +31 -32
- package/dist/auth/errors.js +3 -1
- package/dist/auth/nbt-auth.js +42 -25
- package/dist/auth/session-store.js +17 -9
- package/dist/cli.js +570 -0
- package/dist/config/data.js +9 -11
- package/dist/config/preferences.js +21 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +64 -38
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +34 -7
- package/dist/core/components/text-input.js +24 -18
- package/dist/core/icons.js +2 -2
- package/dist/core/logo.js +23 -5
- package/dist/core/motion.js +25 -19
- package/dist/core/text.js +186 -69
- package/dist/core/theme.js +0 -28
- package/dist/core/transitions.js +2 -2
- package/dist/core/ui.js +15 -13
- package/dist/core/vim-keys.js +156 -19
- package/dist/features/about.js +23 -0
- package/dist/features/calendar-heatmap.js +16 -40
- package/dist/features/calendar-query.js +1 -2
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +66 -190
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +615 -298
- package/dist/features/links.js +44 -29
- package/dist/features/schedule-render.js +65 -101
- package/dist/features/schedule-store.js +51 -9
- package/dist/features/schedule-view.js +46 -213
- package/dist/features/status.js +117 -60
- package/dist/features/student-timetable.js +74 -97
- package/dist/features/theme.js +9 -5
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +12 -29
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +8 -3
- package/dist/i18n/locales/zh.json +8 -3
- package/dist/index.js +6 -474
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +28 -18
- package/bin/nbtca-welcome.js +0 -2
- package/dist/core/components/screen.js +0 -18
- package/dist/core/menu.js +0 -68
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -127
- package/dist/logo/ca-logo.png +0 -0
package/dist/app/views/events.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pickIcon } from '../../core/icons.js';
|
|
|
7
7
|
import { t } from '../../i18n/index.js';
|
|
8
8
|
import { loadCalendarOrThrow, toDisplayEvent, exportEventIcs } from '../../features/calendar.js';
|
|
9
9
|
import { weekRange, monthRange, filterEvents } from '../../features/calendar-query.js';
|
|
10
|
+
import { addLocalDays } from '../../core/calendar-day.js';
|
|
10
11
|
let state = { mode: 'loading' };
|
|
11
12
|
let calendar = null;
|
|
12
13
|
let currentList = [];
|
|
@@ -29,36 +30,42 @@ function buildListField(title, events, maxVisible) {
|
|
|
29
30
|
const trans = t();
|
|
30
31
|
const display = events.map(toDisplayEvent);
|
|
31
32
|
const options = [
|
|
32
|
-
...
|
|
33
|
-
value: String(
|
|
34
|
-
label: `${
|
|
35
|
-
hint:
|
|
33
|
+
...display.map((event, index) => ({
|
|
34
|
+
value: String(index),
|
|
35
|
+
label: `${event.date}${event.time ? ` ${event.time}` : ''} ${event.title}`,
|
|
36
|
+
hint: event.location,
|
|
36
37
|
})),
|
|
37
38
|
{ value: '__back__', label: backLabel() },
|
|
38
39
|
];
|
|
39
40
|
return new ListField({
|
|
40
41
|
title: title || trans.menu.events,
|
|
41
|
-
options: options.length > 1
|
|
42
|
+
options: options.length > 1
|
|
43
|
+
? options
|
|
44
|
+
: [{ value: '__back__', label: `${trans.calendar.noEvents} — ${backLabel()}` }],
|
|
42
45
|
maxVisible,
|
|
43
46
|
});
|
|
44
47
|
}
|
|
45
48
|
function showList(title, events, ctx) {
|
|
46
49
|
currentList = events;
|
|
47
|
-
state = {
|
|
50
|
+
state = {
|
|
51
|
+
mode: 'list',
|
|
52
|
+
listField: buildListField(title, events, computeMaxVisible(ctx.bodyRows)),
|
|
53
|
+
};
|
|
48
54
|
}
|
|
49
|
-
// A glance-panel ceiling, not "browse everything" (this tab's own "Events"
|
|
50
|
-
// list is for that) — just needs to be at least as many as the tallest
|
|
51
|
-
// reasonable terminal could fit; renderHubBody trims further based on the
|
|
52
|
-
// real ctx.bodyRows.
|
|
53
55
|
const RECENT_ACTIVITY_FETCH_CAP = 15;
|
|
54
56
|
function goToHub() {
|
|
55
57
|
const upcoming = calendar ? calendar.upcoming({ days: 30 }) : [];
|
|
58
|
+
const nextEvent = upcoming[0];
|
|
56
59
|
state = {
|
|
57
60
|
mode: 'hub',
|
|
58
61
|
hubField: buildHubField(),
|
|
59
|
-
nextEvent
|
|
62
|
+
...(nextEvent === undefined ? {} : { nextEvent: toDisplayEvent(nextEvent) }),
|
|
60
63
|
heatmapBuckets: calendar
|
|
61
|
-
? calendar.heatmap({
|
|
64
|
+
? calendar.heatmap({
|
|
65
|
+
start: addLocalDays(new Date(), -365),
|
|
66
|
+
end: new Date(),
|
|
67
|
+
bucket: 'day',
|
|
68
|
+
})
|
|
62
69
|
: [],
|
|
63
70
|
recentEvents: upcoming.slice(0, RECENT_ACTIVITY_FETCH_CAP).map(toDisplayEvent),
|
|
64
71
|
};
|
|
@@ -74,10 +81,6 @@ function showDetail(raw) {
|
|
|
74
81
|
detailDescription: e.description,
|
|
75
82
|
detailEvent: raw,
|
|
76
83
|
detailField: new ListField({
|
|
77
|
-
// Empty, not e.title: renderEvents already prints the event title as
|
|
78
|
-
// its own heading right above (detailTitle) -- giving the field the
|
|
79
|
-
// same string a second time repeated it verbatim just above the
|
|
80
|
-
// Export/Back options.
|
|
81
84
|
title: '',
|
|
82
85
|
options: [
|
|
83
86
|
{ value: 'export', label: trans.calendar.exportIcs },
|
|
@@ -90,39 +93,49 @@ export const eventsView = {
|
|
|
90
93
|
id: 'events',
|
|
91
94
|
title: t().menu.events,
|
|
92
95
|
async load(ctx) {
|
|
96
|
+
if (ctx.signal?.aborted)
|
|
97
|
+
return;
|
|
93
98
|
state = { mode: 'loading' };
|
|
94
99
|
ctx.rerender();
|
|
95
100
|
try {
|
|
96
|
-
|
|
101
|
+
const loadedCalendar = await loadCalendarOrThrow(ctx.signal);
|
|
102
|
+
if (ctx.signal?.aborted)
|
|
103
|
+
return;
|
|
104
|
+
calendar = loadedCalendar;
|
|
97
105
|
goToHub();
|
|
98
106
|
}
|
|
99
107
|
catch {
|
|
108
|
+
if (ctx.signal?.aborted)
|
|
109
|
+
return;
|
|
100
110
|
state = { mode: 'error', errorMessage: t().calendar.error };
|
|
101
111
|
}
|
|
102
|
-
ctx.
|
|
112
|
+
if (!ctx.signal?.aborted)
|
|
113
|
+
ctx.rerender();
|
|
103
114
|
},
|
|
104
115
|
render(ctx) {
|
|
105
|
-
// Sync the list's scroll window to the *current* terminal size on every
|
|
106
|
-
// frame (not just construction time) — this is what keeps a long list
|
|
107
|
-
// correctly windowed across a live resize.
|
|
108
116
|
state.listField?.setMaxVisible(computeMaxVisible(ctx.bodyRows));
|
|
109
117
|
return renderEvents(state, new Date(), ctx.bodyRows, ctx.size.cols);
|
|
110
118
|
},
|
|
119
|
+
isBusy() {
|
|
120
|
+
return state.mode === 'loading';
|
|
121
|
+
},
|
|
111
122
|
capturesInput() {
|
|
112
123
|
return state.mode === 'search';
|
|
113
124
|
},
|
|
125
|
+
capturesPageKeys() {
|
|
126
|
+
return state.mode === 'hub' || state.mode === 'list' || state.mode === 'detail';
|
|
127
|
+
},
|
|
114
128
|
footerHint(tabCount, cols = Number.POSITIVE_INFINITY) {
|
|
115
129
|
if (state.mode === 'search')
|
|
116
130
|
return captureFooterHint(cols);
|
|
117
|
-
// A pure "read this, any key returns to the hub" drill-down (see
|
|
118
|
-
// handleKey below) -- no field to move a cursor within or open an item
|
|
119
|
-
// from, so the generic "move · open" hint would promise keys that
|
|
120
|
-
// don't do that here.
|
|
121
131
|
const passive = state.mode === 'loading' || state.mode === 'error' || state.mode === 'heatmap';
|
|
122
132
|
return passive ? passiveFooterHint(tabCount, cols) : undefined;
|
|
123
133
|
},
|
|
124
134
|
handleBack() {
|
|
125
|
-
if (state.mode === 'list' ||
|
|
135
|
+
if (state.mode === 'list' ||
|
|
136
|
+
state.mode === 'detail' ||
|
|
137
|
+
state.mode === 'search' ||
|
|
138
|
+
state.mode === 'heatmap') {
|
|
126
139
|
if (state.mode === 'search')
|
|
127
140
|
setVimKeysActive(true);
|
|
128
141
|
goToHub();
|
|
@@ -163,12 +176,17 @@ export const eventsView = {
|
|
|
163
176
|
}
|
|
164
177
|
if (result.selected === 'search') {
|
|
165
178
|
setVimKeysActive(false);
|
|
166
|
-
state = {
|
|
179
|
+
state = {
|
|
180
|
+
mode: 'search',
|
|
181
|
+
searchField: new TextField({
|
|
182
|
+
message: t().calendar.searchPrompt,
|
|
183
|
+
placeholder: t().calendar.searchPlaceholder,
|
|
184
|
+
allowEmpty: true,
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
167
187
|
}
|
|
168
188
|
return;
|
|
169
189
|
}
|
|
170
|
-
// A pure detail/drill-down view with no field of its own — any key
|
|
171
|
-
// returns to the hub, matching Schedule's 'week'/'unresolved' modes.
|
|
172
190
|
case 'heatmap': {
|
|
173
191
|
goToHub();
|
|
174
192
|
return;
|
|
@@ -196,7 +214,12 @@ export const eventsView = {
|
|
|
196
214
|
}
|
|
197
215
|
if (result.selected === 'export' && state.detailEvent) {
|
|
198
216
|
const res = exportEventIcs(state.detailEvent);
|
|
199
|
-
state = {
|
|
217
|
+
state = {
|
|
218
|
+
...state,
|
|
219
|
+
statusMessage: res.ok
|
|
220
|
+
? `${t().calendar.exportSuccess}: ${res.path}`
|
|
221
|
+
: `${t().calendar.exportError}: ${res.error ?? ''}`,
|
|
222
|
+
};
|
|
200
223
|
}
|
|
201
224
|
return;
|
|
202
225
|
}
|
|
@@ -210,18 +233,19 @@ export const eventsView = {
|
|
|
210
233
|
if (result?.submitted !== undefined) {
|
|
211
234
|
setVimKeysActive(true);
|
|
212
235
|
const query = result.submitted.trim();
|
|
213
|
-
if (!query
|
|
236
|
+
if (!query) {
|
|
214
237
|
goToHub();
|
|
215
238
|
return;
|
|
216
239
|
}
|
|
217
240
|
const now = new Date();
|
|
218
|
-
const pool = calendar.inRange(now,
|
|
241
|
+
const pool = calendar.inRange(now, addLocalDays(now, 365));
|
|
219
242
|
const results = filterEvents(pool, query);
|
|
220
243
|
showList(`${t().calendar.search}: ${query}`, results, ctx);
|
|
221
244
|
}
|
|
222
245
|
return;
|
|
223
246
|
}
|
|
224
|
-
|
|
247
|
+
case 'loading':
|
|
248
|
+
case 'error':
|
|
225
249
|
return;
|
|
226
250
|
}
|
|
227
251
|
},
|
package/dist/app/views/home.js
CHANGED
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
import { c, type, space, glyph } from '../../core/theme.js';
|
|
2
2
|
import { t } from '../../i18n/index.js';
|
|
3
3
|
import { pickIcon } from '../../core/icons.js';
|
|
4
|
-
import { padEndV, visualWidth,
|
|
5
|
-
import { peekNextClassLine, peekTodayLines, peekWeekAheadInfo, peekUnresolvedCount } from '../../features/schedule-view.js';
|
|
6
|
-
import { loadCalendarOrThrow, toDisplayEvent, renderEventBrief } from '../../features/calendar.js';
|
|
4
|
+
import { padEndV, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
5
|
+
import { peekNextClassLine, peekTodayLines, peekWeekAheadInfo, peekUnresolvedCount, } from '../../features/schedule-view.js';
|
|
6
|
+
import { loadCalendarOrThrow, peekCalendar, toDisplayEvent, renderEventBrief, } from '../../features/calendar.js';
|
|
7
7
|
import { weekdayShortLabel } from '../../features/schedule-render.js';
|
|
8
|
-
import {
|
|
8
|
+
import { addLocalDays } from '../../core/calendar-day.js';
|
|
9
9
|
import { passiveFooterHint } from '../chrome.js';
|
|
10
|
+
import { campusWeekday } from '@nbtca/nbtcal/timetable';
|
|
11
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
12
|
+
const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
|
10
13
|
function wrappedIndentedLines(label, cols, style) {
|
|
11
|
-
|
|
12
|
-
const styled = style(label);
|
|
13
|
-
const preferredIndent = visualWidth(space.indent) < width ? space.indent : '';
|
|
14
|
-
const indent = preferredIndent
|
|
15
|
-
&& visualWidth(styled) > width - visualWidth(preferredIndent)
|
|
16
|
-
&& visualWidth(styled) <= width
|
|
17
|
-
? ''
|
|
18
|
-
: preferredIndent;
|
|
19
|
-
const contentWidth = Math.max(1, width - visualWidth(indent));
|
|
20
|
-
return wrapAnsiToVisualWidth(styled, contentWidth).map((line) => `${indent}${line}`);
|
|
14
|
+
return wrapAnsiWithIndent(style(label), cols, space.indent);
|
|
21
15
|
}
|
|
22
16
|
function panelHeading(label, cols) {
|
|
23
17
|
return wrappedIndentedLines(label, cols, type.heading);
|
|
24
18
|
}
|
|
25
|
-
function loadingLines(cols) {
|
|
26
|
-
return wrappedIndentedLines(t().common.loading, cols, type.hint);
|
|
27
|
-
}
|
|
28
19
|
function wrappedRenderedLines(line, cols) {
|
|
29
20
|
const content = line.startsWith(space.indent) ? line.slice(space.indent.length) : line;
|
|
30
21
|
return wrappedIndentedLines(content, cols, (value) => value);
|
|
@@ -39,7 +30,9 @@ function renderDayProgress(now, cols) {
|
|
|
39
30
|
const indent = visualWidth(space.indent) + 2 + visualWidth(percentage) + 1 <= width ? space.indent : '';
|
|
40
31
|
const gap = visualWidth(indent) + 2 + visualWidth(percentage) + 1 <= width
|
|
41
32
|
? ' '
|
|
42
|
-
: visualWidth(indent) + 1 + visualWidth(percentage) + 1 <= width
|
|
33
|
+
: visualWidth(indent) + 1 + visualWidth(percentage) + 1 <= width
|
|
34
|
+
? ' '
|
|
35
|
+
: '';
|
|
43
36
|
const barWidth = Math.max(0, Math.min(DAY_PROGRESS_WIDTH, width - visualWidth(indent) - visualWidth(gap) - visualWidth(percentage)));
|
|
44
37
|
const filled = Math.round(fraction * barWidth);
|
|
45
38
|
const filledChar = glyph.barFilled();
|
|
@@ -57,17 +50,21 @@ function renderWeekAheadGrid(classDays, eventDays, cols) {
|
|
|
57
50
|
const days = [1, 2, 3, 4, 5, 6, 7];
|
|
58
51
|
const dayLabels = days.map((wd) => type.hint(weekdayShortLabel(wd))).join(' ');
|
|
59
52
|
const headerLine = `${space.indent}${padEndV('', rowLabelW)}${dayLabels}`;
|
|
60
|
-
const classCells = days
|
|
53
|
+
const classCells = days
|
|
54
|
+
.map((wd) => {
|
|
61
55
|
const isWeekend = wd === 6 || wd === 7;
|
|
62
|
-
const glyphChar = isWeekend ? weekendChar :
|
|
56
|
+
const glyphChar = isWeekend ? weekendChar : classDays[wd - 1] ? hasClassChar : freeChar;
|
|
63
57
|
return type.body(glyphChar);
|
|
64
|
-
})
|
|
58
|
+
})
|
|
59
|
+
.join(' ');
|
|
65
60
|
const classLine = `${space.indent}${type.hint(padEndV(trans.timetable.weekAheadClasses, rowLabelW))}${classCells}`;
|
|
66
|
-
const eventCells = days
|
|
61
|
+
const eventCells = days
|
|
62
|
+
.map((wd) => {
|
|
67
63
|
if (!eventDays)
|
|
68
64
|
return blankCell;
|
|
69
65
|
return type.body(eventDays[wd - 1] ? hasClassChar : freeChar);
|
|
70
|
-
})
|
|
66
|
+
})
|
|
67
|
+
.join(' ');
|
|
71
68
|
const eventLine = `${space.indent}${type.hint(padEndV(trans.menu.events, rowLabelW))}${eventCells}`;
|
|
72
69
|
const legend = `${space.indent}${type.hint(`${hasClassChar} ${trans.timetable.weekAheadBusy} ${freeChar} ${trans.timetable.weekAheadFree} ${weekendChar} ${trans.timetable.weekAheadNone}`)}`;
|
|
73
70
|
const wideLines = [headerLine, classLine, eventLine, legend];
|
|
@@ -76,12 +73,8 @@ function renderWeekAheadGrid(classDays, eventDays, cols) {
|
|
|
76
73
|
return wideLines.join('\n');
|
|
77
74
|
const compactHeading = wrappedIndentedLines(`${trans.timetable.weekAheadClasses} / ${trans.menu.events}`, cols, type.hint);
|
|
78
75
|
const compactDays = days.flatMap((wd) => {
|
|
79
|
-
const classCell = wd === 6 || wd === 7
|
|
80
|
-
|
|
81
|
-
: classDays[wd - 1] ? hasClassChar : freeChar;
|
|
82
|
-
const eventCell = eventDays
|
|
83
|
-
? eventDays[wd - 1] ? hasClassChar : freeChar
|
|
84
|
-
: blankCell;
|
|
76
|
+
const classCell = wd === 6 || wd === 7 ? weekendChar : classDays[wd - 1] ? hasClassChar : freeChar;
|
|
77
|
+
const eventCell = eventDays ? (eventDays[wd - 1] ? hasClassChar : freeChar) : blankCell;
|
|
85
78
|
const row = `${type.hint(weekdayShortLabel(wd))} ${type.body(classCell)} ${type.body(eventCell)}`;
|
|
86
79
|
return wrappedIndentedLines(row, cols, (value) => value);
|
|
87
80
|
});
|
|
@@ -116,7 +109,10 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
116
109
|
lines.push(...wrappedIndentedLines(`${pickIcon('⚠', '!')} ${trans.timetable.hubUnresolved} · ${data.unresolvedCount}`, cols, c.warn));
|
|
117
110
|
lines.push('');
|
|
118
111
|
}
|
|
119
|
-
|
|
112
|
+
const eventsStale = data.eventsLoadFailed === true && (data.eventLines?.length ?? 0) > 0;
|
|
113
|
+
lines.push(...(eventsStale
|
|
114
|
+
? wrappedIndentedLines(`${type.heading(trans.menu.events)} ${type.hint(`${pickIcon('·', '-')} ${trans.calendar.stale}`)}`, cols, (value) => value)
|
|
115
|
+
: panelHeading(trans.menu.events, cols)));
|
|
120
116
|
if (data.eventLines && data.eventLines.length > 0) {
|
|
121
117
|
const remaining = Number.isFinite(bodyRows)
|
|
122
118
|
? Math.max(0, Math.floor(bodyRows) - lines.length)
|
|
@@ -134,7 +130,7 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
134
130
|
}
|
|
135
131
|
}
|
|
136
132
|
else if (data.loading) {
|
|
137
|
-
lines.push(...loadingLines(cols));
|
|
133
|
+
lines.push(...loadingLines(trans.common.loading, cols));
|
|
138
134
|
}
|
|
139
135
|
else if (data.eventsLoadFailed) {
|
|
140
136
|
lines.push(...wrappedIndentedLines(trans.calendar.error, cols, type.hint));
|
|
@@ -144,6 +140,25 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
144
140
|
}
|
|
145
141
|
return lines;
|
|
146
142
|
}
|
|
143
|
+
const HOME_EVENT_FETCH_CAP = 15;
|
|
144
|
+
function calendarSnapshot(cal, weekAheadInfo) {
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const eventLines = cal
|
|
147
|
+
.upcoming({ days: 30 })
|
|
148
|
+
.slice(0, HOME_EVENT_FETCH_CAP)
|
|
149
|
+
.map((event) => renderEventBrief(toDisplayEvent(event), now));
|
|
150
|
+
if (!weekAheadInfo)
|
|
151
|
+
return { eventLines };
|
|
152
|
+
const weekEnd = addLocalDays(weekAheadInfo.weekStartDate, 7);
|
|
153
|
+
const daySet = new Set(cal.inRange(weekAheadInfo.weekStartDate, weekEnd).map((event) => campusWeekday(event.start)));
|
|
154
|
+
return {
|
|
155
|
+
eventLines,
|
|
156
|
+
weekAhead: {
|
|
157
|
+
classDays: weekAheadInfo.classDays,
|
|
158
|
+
eventDays: WEEKDAYS.map((weekday) => daySet.has(weekday)),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
147
162
|
let data = { loading: true };
|
|
148
163
|
export const homeView = {
|
|
149
164
|
id: 'home',
|
|
@@ -152,6 +167,8 @@ export const homeView = {
|
|
|
152
167
|
return passiveFooterHint(tabCount, cols);
|
|
153
168
|
},
|
|
154
169
|
async load(ctx) {
|
|
170
|
+
if (ctx.signal?.aborted)
|
|
171
|
+
return;
|
|
155
172
|
const weekAheadInfo = peekWeekAheadInfo();
|
|
156
173
|
try {
|
|
157
174
|
data = {
|
|
@@ -159,36 +176,39 @@ export const homeView = {
|
|
|
159
176
|
nextClassLine: peekNextClassLine(),
|
|
160
177
|
todayLines: peekTodayLines(),
|
|
161
178
|
unresolvedCount: peekUnresolvedCount(),
|
|
162
|
-
|
|
179
|
+
...(weekAheadInfo ? { weekAhead: { classDays: weekAheadInfo.classDays } } : {}),
|
|
163
180
|
};
|
|
164
181
|
}
|
|
165
182
|
catch {
|
|
166
183
|
data = { loading: true };
|
|
167
184
|
}
|
|
185
|
+
const cached = peekCalendar();
|
|
186
|
+
if (cached)
|
|
187
|
+
data = { ...data, ...calendarSnapshot(cached, weekAheadInfo) };
|
|
188
|
+
if (ctx.signal?.aborted)
|
|
189
|
+
return;
|
|
168
190
|
ctx.rerender();
|
|
169
|
-
const HOME_EVENT_FETCH_CAP = 15;
|
|
170
191
|
try {
|
|
171
|
-
const cal = await loadCalendarOrThrow();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let weekAhead = data.weekAhead;
|
|
176
|
-
if (weekAheadInfo) {
|
|
177
|
-
const weekEnd = new Date(weekAheadInfo.weekStartDate.getTime() + 7 * 86400000);
|
|
178
|
-
const weekEvents = cal.inRange(weekAheadInfo.weekStartDate, weekEnd);
|
|
179
|
-
const daySet = new Set(weekEvents.map((e) => campusWeekday(e.start)));
|
|
180
|
-
weekAhead = { classDays: weekAheadInfo.classDays, eventDays: [1, 2, 3, 4, 5, 6, 7].map((wd) => daySet.has(wd)) };
|
|
181
|
-
}
|
|
182
|
-
data = { ...data, eventLines, weekAhead };
|
|
192
|
+
const cal = await loadCalendarOrThrow(ctx.signal);
|
|
193
|
+
if (ctx.signal?.aborted)
|
|
194
|
+
return;
|
|
195
|
+
data = { ...data, ...calendarSnapshot(cal, weekAheadInfo) };
|
|
183
196
|
}
|
|
184
197
|
catch {
|
|
198
|
+
if (ctx.signal?.aborted)
|
|
199
|
+
return;
|
|
185
200
|
data = { ...data, eventsLoadFailed: true };
|
|
186
201
|
}
|
|
187
202
|
finally {
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
if (!ctx.signal?.aborted) {
|
|
204
|
+
data = { ...data, loading: false };
|
|
205
|
+
ctx.rerender();
|
|
206
|
+
}
|
|
190
207
|
}
|
|
191
208
|
},
|
|
209
|
+
isBusy() {
|
|
210
|
+
return data.loading === true;
|
|
211
|
+
},
|
|
192
212
|
render(ctx) {
|
|
193
213
|
return renderHome(data, new Date(), ctx.bodyRows, ctx.size.cols);
|
|
194
214
|
},
|
|
@@ -1,40 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createTimetableSchedule, } from '@nbtca/nbtcal/timetable';
|
|
2
2
|
export const KEY_ARROW_LEFT = '\x1b[D';
|
|
3
3
|
export const KEY_ARROW_RIGHT = '\x1b[C';
|
|
4
4
|
export const KEY_ARROW_UP = '\x1b[A';
|
|
5
5
|
export const KEY_ARROW_DOWN = '\x1b[B';
|
|
6
6
|
export const KEY_ENTER_CR = '\r';
|
|
7
7
|
export const KEY_ENTER_LF = '\n';
|
|
8
|
-
/** The cursor's starting position on entering hub/week mode: today's own
|
|
9
|
-
* weekday (falling back to Monday on a weekend, since the grid's Sat/Sun
|
|
10
|
-
* columns are always empty for a personal timetable) and the first period
|
|
11
|
-
* this term's own period table actually defines. */
|
|
12
8
|
export function defaultGridCursor(todayWeekday, periods) {
|
|
13
9
|
const sorted = [...periods].sort((a, b) => a.period - b.period);
|
|
14
10
|
const firstPeriod = sorted[0]?.period ?? 1;
|
|
15
|
-
const weekday = todayWeekday >= 1 && todayWeekday <= 5 ? todayWeekday : 1;
|
|
11
|
+
const weekday = (todayWeekday >= 1 && todayWeekday <= 5 ? todayWeekday : 1);
|
|
16
12
|
return { weekday, period: firstPeriod };
|
|
17
13
|
}
|
|
18
|
-
/** Moves the cursor one weekday left/right, clamped to [1, 7] with no
|
|
19
|
-
* wraparound -- a 7-day week has a real fixed edge, unlike a scrollable
|
|
20
|
-
* list where wrapping back to the top makes sense. */
|
|
21
14
|
export function moveCursorWeekday(cursor, delta) {
|
|
22
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
...cursor,
|
|
17
|
+
weekday: Math.max(1, Math.min(7, cursor.weekday + delta)),
|
|
18
|
+
};
|
|
23
19
|
}
|
|
24
|
-
/** Moves the cursor to the previous/next *defined* period in the sorted
|
|
25
|
-
* period table (not period±1 -- real period numbers aren't always
|
|
26
|
-
* contiguous), clamped at the first/last period with no wraparound. */
|
|
27
20
|
export function moveCursorPeriod(cursor, periods, delta) {
|
|
28
21
|
const sorted = [...periods].sort((a, b) => a.period - b.period);
|
|
29
22
|
if (sorted.length === 0)
|
|
30
23
|
return cursor;
|
|
31
24
|
const idx = sorted.findIndex((p) => p.period === cursor.period);
|
|
32
25
|
const nextIdx = Math.max(0, Math.min(sorted.length - 1, (idx === -1 ? 0 : idx) + delta));
|
|
33
|
-
|
|
26
|
+
const nextPeriod = sorted[nextIdx];
|
|
27
|
+
return nextPeriod ? { ...cursor, period: nextPeriod.period } : cursor;
|
|
34
28
|
}
|
|
35
|
-
/** Pure key-to-action mapping shared by hub mode's inline grid and the
|
|
36
|
-
* standalone full-screen 'week' mode -- both are cursor-navigable over the
|
|
37
|
-
* exact same rules, so this is the one place that logic lives. */
|
|
38
29
|
export function handleGridKey(key, cursor, tt, week) {
|
|
39
30
|
if (key === KEY_ARROW_LEFT)
|
|
40
31
|
return { kind: 'moveCursor', cursor: moveCursorWeekday(cursor, -1) };
|
|
@@ -45,7 +36,7 @@ export function handleGridKey(key, cursor, tt, week) {
|
|
|
45
36
|
if (key === KEY_ARROW_DOWN)
|
|
46
37
|
return { kind: 'moveCursor', cursor: moveCursorPeriod(cursor, tt.periods, 1) };
|
|
47
38
|
if (key === KEY_ENTER_CR || key === KEY_ENTER_LF) {
|
|
48
|
-
const meeting =
|
|
39
|
+
const meeting = createTimetableSchedule(tt).meetingAt(week, cursor.weekday, cursor.period);
|
|
49
40
|
return meeting ? { kind: 'openDetail', meeting } : { kind: 'none' };
|
|
50
41
|
}
|
|
51
42
|
return { kind: 'none' };
|