@walavave/cc98-cli 0.2.3 → 0.2.4

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/README.md +6 -1
  3. package/dist/api/client.d.ts +9 -2
  4. package/dist/api/client.d.ts.map +1 -1
  5. package/dist/api/client.js +42 -4
  6. package/dist/api/client.js.map +1 -1
  7. package/dist/api/endpoints.d.ts +6 -0
  8. package/dist/api/endpoints.d.ts.map +1 -1
  9. package/dist/api/endpoints.js +6 -0
  10. package/dist/api/endpoints.js.map +1 -1
  11. package/dist/tui/app-data.d.ts +7 -26
  12. package/dist/tui/app-data.d.ts.map +1 -1
  13. package/dist/tui/app-data.js +7 -1100
  14. package/dist/tui/app-data.js.map +1 -1
  15. package/dist/tui/app-runtime/content/list.d.ts +5 -0
  16. package/dist/tui/app-runtime/content/list.d.ts.map +1 -0
  17. package/dist/tui/app-runtime/content/list.js +115 -0
  18. package/dist/tui/app-runtime/content/list.js.map +1 -0
  19. package/dist/tui/app-runtime/content/navigation.d.ts +5 -0
  20. package/dist/tui/app-runtime/content/navigation.d.ts.map +1 -0
  21. package/dist/tui/app-runtime/content/navigation.js +104 -0
  22. package/dist/tui/app-runtime/content/navigation.js.map +1 -0
  23. package/dist/tui/app-runtime/content/search.d.ts +4 -0
  24. package/dist/tui/app-runtime/content/search.d.ts.map +1 -0
  25. package/dist/tui/app-runtime/content/search.js +122 -0
  26. package/dist/tui/app-runtime/content/search.js.map +1 -0
  27. package/dist/tui/app-runtime/content.d.ts +3 -8
  28. package/dist/tui/app-runtime/content.d.ts.map +1 -1
  29. package/dist/tui/app-runtime/content.js +3 -286
  30. package/dist/tui/app-runtime/content.js.map +1 -1
  31. package/dist/tui/app-runtime/interactions.d.ts.map +1 -1
  32. package/dist/tui/app-runtime/interactions.js +5 -17
  33. package/dist/tui/app-runtime/interactions.js.map +1 -1
  34. package/dist/tui/app-runtime/keyboard.d.ts.map +1 -1
  35. package/dist/tui/app-runtime/keyboard.js +32 -10
  36. package/dist/tui/app-runtime/keyboard.js.map +1 -1
  37. package/dist/tui/app-runtime/me.d.ts +5 -0
  38. package/dist/tui/app-runtime/me.d.ts.map +1 -0
  39. package/dist/tui/app-runtime/me.js +83 -0
  40. package/dist/tui/app-runtime/me.js.map +1 -0
  41. package/dist/tui/app-runtime/modals.d.ts.map +1 -1
  42. package/dist/tui/app-runtime/modals.js +27 -11
  43. package/dist/tui/app-runtime/modals.js.map +1 -1
  44. package/dist/tui/app-runtime/mouse.d.ts.map +1 -1
  45. package/dist/tui/app-runtime/mouse.js +8 -2
  46. package/dist/tui/app-runtime/mouse.js.map +1 -1
  47. package/dist/tui/app-runtime/state.d.ts.map +1 -1
  48. package/dist/tui/app-runtime/state.js +3 -1
  49. package/dist/tui/app-runtime/state.js.map +1 -1
  50. package/dist/tui/app-runtime/topic.d.ts.map +1 -1
  51. package/dist/tui/app-runtime/topic.js +41 -1
  52. package/dist/tui/app-runtime/topic.js.map +1 -1
  53. package/dist/tui/app.d.ts.map +1 -1
  54. package/dist/tui/app.js +5 -1
  55. package/dist/tui/app.js.map +1 -1
  56. package/dist/tui/cached-client.d.ts +11 -0
  57. package/dist/tui/cached-client.d.ts.map +1 -1
  58. package/dist/tui/cached-client.js +47 -0
  59. package/dist/tui/cached-client.js.map +1 -1
  60. package/dist/tui/data/accounts.d.ts +6 -0
  61. package/dist/tui/data/accounts.d.ts.map +1 -0
  62. package/dist/tui/data/accounts.js +30 -0
  63. package/dist/tui/data/accounts.js.map +1 -0
  64. package/dist/tui/data/content.d.ts +9 -0
  65. package/dist/tui/data/content.d.ts.map +1 -0
  66. package/dist/tui/data/content.js +196 -0
  67. package/dist/tui/data/content.js.map +1 -0
  68. package/dist/tui/data/feed-status.d.ts +3 -0
  69. package/dist/tui/data/feed-status.d.ts.map +1 -0
  70. package/dist/tui/data/feed-status.js +37 -0
  71. package/dist/tui/data/feed-status.js.map +1 -0
  72. package/dist/tui/data/items.d.ts +6 -0
  73. package/dist/tui/data/items.d.ts.map +1 -0
  74. package/dist/tui/data/items.js +67 -0
  75. package/dist/tui/data/items.js.map +1 -0
  76. package/dist/tui/data/me.d.ts +8 -0
  77. package/dist/tui/data/me.d.ts.map +1 -0
  78. package/dist/tui/data/me.js +167 -0
  79. package/dist/tui/data/me.js.map +1 -0
  80. package/dist/tui/data/navigation-state.d.ts +12 -0
  81. package/dist/tui/data/navigation-state.d.ts.map +1 -0
  82. package/dist/tui/data/navigation-state.js +118 -0
  83. package/dist/tui/data/navigation-state.js.map +1 -0
  84. package/dist/tui/data/search.d.ts +5 -0
  85. package/dist/tui/data/search.d.ts.map +1 -0
  86. package/dist/tui/data/search.js +89 -0
  87. package/dist/tui/data/search.js.map +1 -0
  88. package/dist/tui/data/topic.d.ts +8 -0
  89. package/dist/tui/data/topic.d.ts.map +1 -0
  90. package/dist/tui/data/topic.js +406 -0
  91. package/dist/tui/data/topic.js.map +1 -0
  92. package/dist/tui/data/utils.d.ts +10 -0
  93. package/dist/tui/data/utils.d.ts.map +1 -0
  94. package/dist/tui/data/utils.js +49 -0
  95. package/dist/tui/data/utils.js.map +1 -0
  96. package/dist/tui/data/view-items.d.ts +16 -0
  97. package/dist/tui/data/view-items.d.ts.map +1 -0
  98. package/dist/tui/data/view-items.js +179 -0
  99. package/dist/tui/data/view-items.js.map +1 -0
  100. package/dist/tui/data/view-loader.d.ts +10 -0
  101. package/dist/tui/data/view-loader.d.ts.map +1 -0
  102. package/dist/tui/data/view-loader.js +157 -0
  103. package/dist/tui/data/view-loader.js.map +1 -0
  104. package/dist/tui/keymap.d.ts +1 -1
  105. package/dist/tui/keymap.d.ts.map +1 -1
  106. package/dist/tui/keymap.js +2 -0
  107. package/dist/tui/keymap.js.map +1 -1
  108. package/dist/tui/renderer.d.ts +2 -0
  109. package/dist/tui/renderer.d.ts.map +1 -1
  110. package/dist/tui/renderer.js +132 -34
  111. package/dist/tui/renderer.js.map +1 -1
  112. package/dist/tui/tui-model.d.ts +22 -0
  113. package/dist/tui/tui-model.d.ts.map +1 -1
  114. package/dist/tui/tui-model.js.map +1 -1
  115. package/dist/tui/ubb-renderer.d.ts.map +1 -1
  116. package/dist/tui/ubb-renderer.js +7 -2
  117. package/dist/tui/ubb-renderer.js.map +1 -1
  118. package/dist/version.d.ts +1 -1
  119. package/dist/version.js +1 -1
  120. package/package.json +1 -1
@@ -1,1101 +1,8 @@
1
- import { emotionPreviewRows, isEmotionAssetPath, loadEmotionPreview, measureEmotionPreview } from "./emotion-preview.js";
2
- import { imagePreviewRows, loadImagePreview, measureImagePreview, supportsImagePreview } from "./image-preview.js";
3
- import { getSidebarWidth } from "./renderer.js";
4
- import { theme } from "./theme.js";
5
- import { currentTopicPost, getStatus, settingsItems } from "./tui-model.js";
6
- import { renderMarkdownToLines, renderUbbToLines } from "./ubb-renderer.js";
7
- export function createSearchState() {
8
- return {
9
- title: "搜索",
10
- query: "",
11
- draft: "",
12
- loaded: 0,
13
- size: 10,
14
- hasMore: false,
15
- searched: false,
16
- focus: "input"
17
- };
18
- }
19
- export async function openTopic(client, state, topicId, render, config, force = false, signal) {
20
- state.mode = "topic";
21
- state.loading = true;
22
- state.loadingMore = false;
23
- state.error = undefined;
24
- state.scroll = 0;
25
- state.imageViewer = undefined;
26
- state.topic = {
27
- topicId,
28
- title: `#${topicId}`,
29
- meta: "",
30
- lines: [],
31
- posts: [],
32
- loaded: 0,
33
- size: 10,
34
- hasMore: true,
35
- imageCount: 0,
36
- linkCount: 0,
37
- floorInput: ""
38
- };
39
- state.status = "正在打开帖子...";
40
- render();
41
- try {
42
- const [topicRaw, postsRaw] = await Promise.all([
43
- client.getTopic(topicId, force, signal),
44
- client.getTopicPosts(topicId, 0, 10, force, signal)
45
- ]);
46
- const topic = asObject(topicRaw);
47
- const posts = asArray(postsRaw);
48
- const reader = buildTopicReader(topicId, topic, posts, 10, config);
49
- state.topic = reader;
50
- state.viewTitle = reader.title;
51
- void loadTopicImagePreviews(state, render, config, state.sidebarWidth);
52
- }
53
- catch (error) {
54
- if (isAbortError(error)) {
55
- return;
56
- }
57
- state.error = error instanceof Error ? error.message : String(error);
58
- state.status = state.history.length > 0
59
- ? "版面读取失败;Esc/Backspace 返回版面列表 h 返回左栏 r 重试"
60
- : "版面读取失败;h 返回左栏 r 重试";
61
- }
62
- finally {
63
- state.loading = false;
64
- if (!state.error && state.mode === "topic" && state.topic) {
65
- state.status = getStatus(state);
66
- }
67
- render();
68
- }
69
- }
70
- export async function openBoard(client, state, boardId, boardTitle, render, force = false, signal, pushParent = true) {
71
- prepareListView(state, {
72
- title: boardTitle,
73
- status: "正在读取版面帖子...",
74
- currentBoard: { boardId, title: boardTitle },
75
- pushParent
76
- });
77
- render();
78
- try {
79
- const topics = asArray(await client.getBoardTopics(boardId, 0, 12, false, force, signal));
80
- state.items = topics.map((topic) => topicItem(topic));
81
- state.status = "版面帖子:j/k 选择 l 打开帖子 h 返回 r 刷新";
82
- }
83
- catch (error) {
84
- if (isAbortError(error)) {
85
- return;
86
- }
87
- state.error = error instanceof Error ? error.message : String(error);
88
- }
89
- finally {
90
- state.loading = false;
91
- render();
92
- }
93
- }
94
- export async function openChat(client, state, userId, title, render, force = false, signal, pushParent = true) {
95
- prepareListView(state, {
96
- title,
97
- status: "正在读取私信...",
98
- currentChat: { userId, title, loaded: 0, size: 10, hasMore: true },
99
- pushParent
100
- });
101
- const chat = state.currentChat;
102
- if (!chat) {
103
- return;
104
- }
105
- render();
106
- try {
107
- const messages = asArray(await client.getChatHistory(userId, 0, 10, force, signal));
108
- state.items = chatMessageItems(messages, title, userId);
109
- chat.loaded = messages.length;
110
- chat.hasMore = messages.length === chat.size;
111
- state.itemIndex = Math.max(0, state.items.length - 1);
112
- state.status = chat.hasMore
113
- ? "私信:j/k 滚动 n/Space 更早消息 Esc/Backspace 返回联系人 h 返回左栏"
114
- : "私信:j/k 滚动 Esc/Backspace 返回联系人 h 返回左栏";
115
- }
116
- catch (error) {
117
- if (isAbortError(error)) {
118
- return;
119
- }
120
- state.error = error instanceof Error ? error.message : String(error);
121
- state.status = "私信读取失败;Esc/Backspace 返回联系人 h 返回左栏 r 重试";
122
- }
123
- finally {
124
- state.loading = false;
125
- render();
126
- }
127
- }
128
- export async function openUserProfile(client, state, userId, render, force = false, signal, pushParent = true) {
129
- prepareListView(state, {
130
- title: `用户 #${userId}`,
131
- status: "正在读取用户信息...",
132
- currentUser: { userId, title: `用户 #${userId}`, loaded: 0, size: 10, hasMore: true },
133
- pushParent
134
- });
135
- const currentUser = state.currentUser;
136
- if (!currentUser) {
137
- return;
138
- }
139
- render();
140
- try {
141
- const [profileRaw, topicsRaw] = await Promise.all([
142
- client.getUserProfile(userId, force, signal),
143
- client.getRecentTopics(userId, 0, currentUser.size + 1, force, signal)
144
- ]);
145
- const profile = asObject(profileRaw);
146
- const recentTopics = asArray(topicsRaw);
147
- const topicItems = recentTopics.slice(0, currentUser.size).map((topic) => topicItem(topic));
148
- const name = String(profile.name ?? profile.userName ?? `#${userId}`).trim() || `#${userId}`;
149
- currentUser.title = name;
150
- currentUser.loaded = topicItems.length;
151
- currentUser.hasMore = recentTopics.length > currentUser.size;
152
- state.viewTitle = name;
153
- state.items = [...buildUserProfileItems(profile), ...topicItems];
154
- state.status = describeUserProfileStatus(state);
155
- }
156
- catch (error) {
157
- if (isAbortError(error)) {
158
- return;
159
- }
160
- state.error = error instanceof Error ? error.message : String(error);
161
- state.status = "用户页读取失败;Esc/Backspace 返回 h 返回左栏 r 重试";
162
- }
163
- finally {
164
- state.loading = false;
165
- render();
166
- }
167
- }
168
- export async function loadNextChatPage(client, state, render, signal) {
169
- if (!state.currentChat || state.loadingMore || !state.currentChat.hasMore) {
170
- return;
171
- }
172
- state.loadingMore = true;
173
- state.status = "正在读取更早私信...";
174
- render();
175
- try {
176
- const chat = state.currentChat;
177
- const messages = asArray(await client.getChatHistory(chat.userId, chat.loaded, chat.size, false, signal));
178
- const olderItems = chatMessageItems(messages, chat.title, chat.userId);
179
- state.items = [...olderItems, ...state.items];
180
- state.itemIndex += olderItems.length;
181
- state.scroll += olderItems.length;
182
- chat.loaded += messages.length;
183
- chat.hasMore = messages.length === chat.size;
184
- state.status = chat.hasMore
185
- ? "私信:j/k 滚动 n/Space 更早消息 Esc/Backspace 返回联系人 h 返回左栏"
186
- : "已到最早私信;j/k 滚动 Esc/Backspace 返回联系人 h 返回左栏";
187
- }
188
- catch (error) {
189
- if (isAbortError(error)) {
190
- return;
191
- }
192
- state.error = error instanceof Error ? error.message : String(error);
193
- state.status = "更早私信读取失败;n/Space 重试 Esc/Backspace 返回联系人";
194
- }
195
- finally {
196
- state.loadingMore = false;
197
- render();
198
- }
199
- }
200
- export async function loadNextUserTopicPage(client, state, render, signal) {
201
- const currentUser = state.currentUser;
202
- if (!currentUser || state.loadingMore || !currentUser.hasMore) {
203
- return;
204
- }
205
- state.loadingMore = true;
206
- state.status = "正在加载更多主题...";
207
- render();
208
- try {
209
- const topics = asArray(await client.getRecentTopics(currentUser.userId, currentUser.loaded, currentUser.size + 1, false, signal));
210
- const nextItems = topics.slice(0, currentUser.size).map((topic) => topicItem(topic));
211
- state.items = [...state.items, ...nextItems];
212
- currentUser.loaded += nextItems.length;
213
- currentUser.hasMore = topics.length > currentUser.size;
214
- state.status = describeUserProfileStatus(state);
215
- }
216
- catch (error) {
217
- if (isAbortError(error)) {
218
- return;
219
- }
220
- state.error = error instanceof Error ? error.message : String(error);
221
- state.status = "加载更多主题失败;n/Space 重试 Esc/Backspace 返回";
222
- }
223
- finally {
224
- state.loadingMore = false;
225
- render();
226
- }
227
- }
228
- export async function executeSearch(client, state, render, force = false, signal) {
229
- const search = state.currentSearch;
230
- if (!search) {
231
- return;
232
- }
233
- const query = search.draft.trim();
234
- search.query = query;
235
- state.error = undefined;
236
- state.itemIndex = 0;
237
- state.scroll = 0;
238
- state.imageViewer = undefined;
239
- if (!query) {
240
- search.loaded = 0;
241
- search.hasMore = false;
242
- search.searched = false;
243
- search.focus = "input";
244
- state.items = [];
245
- state.loading = false;
246
- state.loadingMore = false;
247
- state.status = "搜索:输入关键词后 Enter 执行 j 进入结果 h 返回左栏";
248
- render();
249
- return;
250
- }
251
- state.loading = true;
252
- state.loadingMore = false;
253
- state.status = `正在搜索 “${query}”...`;
254
- render();
255
- try {
256
- const topics = asArray(await client.searchTopics(query, 0, search.size, force, signal));
257
- state.items = topics.map((topic) => topicItem(topic));
258
- search.loaded = topics.length;
259
- search.hasMore = topics.length === search.size;
260
- search.searched = true;
261
- search.focus = state.items.length > 0 ? "results" : "input";
262
- state.status = state.items.length === 0
263
- ? `未找到 “${query}” 的相关帖子`
264
- : search.hasMore
265
- ? `搜索结果:${state.items.length} 项 j/k 选择 Enter 打开 n/Space 继续加载`
266
- : `搜索结果:${state.items.length} 项 j/k 选择 Enter 打开`;
267
- }
268
- catch (error) {
269
- if (isAbortError(error)) {
270
- return;
271
- }
272
- state.error = error instanceof Error ? error.message : String(error);
273
- state.status = "搜索失败;Enter 重试 h 返回左栏";
274
- }
275
- finally {
276
- state.loading = false;
277
- render();
278
- }
279
- }
280
- export async function loadNextSearchPage(client, state, render, signal) {
281
- const search = state.currentSearch;
282
- if (!search || !search.query || state.loading || state.loadingMore || !search.hasMore) {
283
- return;
284
- }
285
- state.loadingMore = true;
286
- state.error = undefined;
287
- state.status = `正在加载 “${search.query}” 的更多结果...`;
288
- render();
289
- try {
290
- const topics = asArray(await client.searchTopics(search.query, search.loaded, search.size, false, signal));
291
- const nextItems = topics.map((topic) => topicItem(topic));
292
- state.items = [...state.items, ...nextItems];
293
- search.loaded += topics.length;
294
- search.hasMore = topics.length === search.size;
295
- if (state.items.length > 0) {
296
- search.focus = "results";
297
- }
298
- state.status = search.hasMore
299
- ? `搜索结果:${state.items.length} 项 j/k 选择 Enter 打开 n/Space 继续加载`
300
- : `搜索结果:${state.items.length} 项 已全部加载`;
301
- }
302
- catch (error) {
303
- if (isAbortError(error)) {
304
- return;
305
- }
306
- state.error = error instanceof Error ? error.message : String(error);
307
- state.status = "加载更多搜索结果失败;n/Space 重试";
308
- }
309
- finally {
310
- state.loadingMore = false;
311
- render();
312
- }
313
- }
314
- export function restorePreviousView(state) {
315
- const snapshot = state.history.pop();
316
- if (!snapshot) {
317
- return;
318
- }
319
- if (snapshot.kind === "topic") {
320
- applyTopicSnapshot(state, snapshot.value);
321
- }
322
- else {
323
- applyListSnapshot(state, snapshot.value);
324
- }
325
- }
326
- export async function loadNextTopicPage(client, state, render, config, signal, advanceAfterLoad = false) {
327
- if (!state.topic || state.loadingMore || !state.topic.hasMore) {
328
- return;
329
- }
330
- state.loadingMore = true;
331
- state.status = "正在加载下一页...";
332
- render();
333
- try {
334
- const previousPostCount = state.topic.posts.length;
335
- const posts = asArray(await client.getTopicPosts(state.topic.topicId, state.topic.loaded, state.topic.size, false, signal));
336
- appendTopicPosts(state.topic, posts, config, state.sidebarWidth);
337
- void loadTopicImagePreviews(state, render, config, state.sidebarWidth);
338
- state.topic.loaded += posts.length;
339
- state.topic.hasMore = posts.length === state.topic.size;
340
- if (advanceAfterLoad && posts.length > 0) {
341
- const nextPost = state.topic.posts[previousPostCount];
342
- if (nextPost) {
343
- state.scroll = nextPost.lineStart;
344
- }
345
- }
346
- }
347
- catch (error) {
348
- if (isAbortError(error)) {
349
- return;
350
- }
351
- state.error = error instanceof Error ? error.message : String(error);
352
- }
353
- finally {
354
- state.loadingMore = false;
355
- if (!state.error && state.mode === "topic" && state.topic) {
356
- state.status = getStatus(state);
357
- }
358
- render();
359
- }
360
- }
361
- export async function jumpToTopicFloor(client, state, floor, render, config, signal) {
362
- const topic = state.topic;
363
- if (!topic) {
364
- return;
365
- }
366
- const loaded = findTopicPostByFloor(topic, floor);
367
- if (loaded) {
368
- state.scroll = loaded.lineStart;
369
- state.status = getStatus(state);
370
- render();
371
- return;
372
- }
373
- const from = Math.floor((floor - 1) / topic.size) * topic.size;
374
- state.loadingMore = true;
375
- state.status = `正在读取 ${floor} 楼...`;
376
- render();
377
- try {
378
- const posts = asArray(await client.getTopicPosts(topic.topicId, from, topic.size, false, signal));
379
- appendTopicPosts(topic, posts, config, state.sidebarWidth);
380
- topic.posts.sort((left, right) => (left.floor ?? 0) - (right.floor ?? 0));
381
- void loadTopicImagePreviews(state, render, config, state.sidebarWidth);
382
- topic.loaded = Math.max(topic.loaded, from + posts.length);
383
- topic.hasMore = posts.length === topic.size;
384
- const target = findTopicPostByFloor(topic, floor);
385
- if (target) {
386
- state.scroll = target.lineStart;
387
- }
388
- else {
389
- state.status = `未找到 ${floor} 楼`;
390
- }
391
- }
392
- catch (error) {
393
- if (!isAbortError(error)) {
394
- state.error = error instanceof Error ? error.message : String(error);
395
- }
396
- }
397
- finally {
398
- state.loadingMore = false;
399
- if (!state.error && findTopicPostByFloor(topic, floor)) {
400
- state.status = getStatus(state);
401
- }
402
- render();
403
- }
404
- }
405
- export function jumpRelativeTopicFloor(state, delta) {
406
- const topic = state.topic;
407
- if (!topic || topic.posts.length === 0) {
408
- return;
409
- }
410
- const current = currentTopicPost(topic, state.scroll);
411
- const currentIndex = current ? topic.posts.indexOf(current) : 0;
412
- const next = topic.posts[Math.min(topic.posts.length - 1, Math.max(0, currentIndex + delta))];
413
- if (next) {
414
- state.scroll = next.lineStart;
415
- }
416
- }
417
- export async function refreshAccounts(state, tokenStore) {
418
- const accounts = await tokenStore.listAccounts();
419
- const current = await tokenStore.getCurrentAccountName();
420
- state.account = current;
421
- state.accountModal.accounts = accounts.map((account) => ({
422
- account: account.account,
423
- detail: account.displayName ?? account.username ?? (account.userId ? `#${account.userId}` : "本地账号"),
424
- isCurrent: account.account === current
425
- }));
426
- state.accountModal.selectedIndex = Math.min(state.accountModal.accounts.findIndex((account) => account.isCurrent), state.accountModal.accounts.length);
427
- if (state.accountModal.selectedIndex < 0) {
428
- state.accountModal.selectedIndex = 0;
429
- }
430
- }
431
- export function getDefaultAccountName(me, username) {
432
- if (typeof me.name === "string" && me.name.trim()) {
433
- return me.name.trim();
434
- }
435
- if (typeof me.id === "number") {
436
- return String(me.id);
437
- }
438
- return username;
439
- }
440
- export function normalizeLoginMessage(error) {
441
- if (error instanceof Error) {
442
- return error.message.replace(/^login failed:\s*/i, "");
443
- }
444
- return String(error);
445
- }
446
- export async function loadView(client, view, force, signal) {
447
- switch (view) {
448
- case "hot": {
449
- const [index, unread] = await Promise.all([
450
- client.getForumIndex(force, signal),
451
- client.getUnreadCount(force, signal)
452
- ]);
453
- const indexObject = asObject(index);
454
- const unreadObject = asObject(unread);
455
- const hotTopics = asArray(indexObject.hotTopic ?? indexObject.manualHotTopic);
456
- return {
457
- title: "十大",
458
- items: hotTopics.map((topic) => topicItem(topic)),
459
- overview: overviewStats(indexObject, unreadObject)
460
- };
461
- }
462
- case "new": {
463
- const topics = asArray(await client.getNewTopics(0, 12, force, signal));
464
- return {
465
- title: "最新",
466
- items: topics.map((topic) => topicItem(topic))
467
- };
468
- }
469
- case "search": {
470
- return {
471
- title: "搜索",
472
- items: [],
473
- status: "搜索:按 Enter 进入输入框并搜索主题"
474
- };
475
- }
476
- case "boards": {
477
- const sections = asArray(await client.getAllBoards(force, signal));
478
- const allBoards = flattenBoards(sections);
479
- return {
480
- title: "版面",
481
- items: allBoards.slice(0, 14),
482
- status: "版面:j/k 选择 l 进入版面 h 返回 r 刷新"
483
- };
484
- }
485
- case "following": {
486
- const topics = asArray(await client.getFolloweeTopics(0, 12, force, signal));
487
- return {
488
- title: "关注",
489
- items: topics.map((topic) => topicItem(topic)),
490
- status: "关注:j/k 选择 l 打开帖子 h 返回 r 刷新"
491
- };
492
- }
493
- case "favorite": {
494
- const [meRaw, sectionsRaw] = await Promise.all([
495
- client.getMe(force, signal),
496
- client.getAllBoards(false, signal)
497
- ]);
498
- const customBoards = asArray(asObject(meRaw).customBoards).filter((id) => typeof id === "number");
499
- const allBoards = flattenBoards(asArray(sectionsRaw));
500
- const boardById = new Map(allBoards.filter((board) => board.boardId !== undefined).map((board) => [board.boardId, board]));
501
- const topicGroups = await mapLimit(customBoards, 3, async (boardId) => {
502
- const board = boardById.get(boardId);
503
- const topics = asArray(await client.getBoardTopics(boardId, 0, 3, false, force, signal));
504
- return topics.map((topic) => topicItem(topic, board));
505
- });
506
- const items = topicGroups.flat().sort((left, right) => (right.sortTime ?? 0) - (left.sortTime ?? 0)).slice(0, 18);
507
- return {
508
- title: "收藏",
509
- items,
510
- status: "收藏:j/k 选择 l 打开帖子 h 返回 r 刷新"
511
- };
512
- }
513
- case "messages": {
514
- const [unread, recent] = await Promise.all([
515
- client.getUnreadCount(force, signal),
516
- client.getRecentChats(0, 10, force, signal)
517
- ]);
518
- const unreadObject = asObject(unread);
519
- const unreadEntries = unreadStats(unreadObject);
520
- const chats = asArray(recent);
521
- const userNames = await loadChatUserNames(client, chats, force, signal);
522
- const unreadItems = unreadEntries
523
- .filter((entry) => entry.detail !== "0" && entry.detail !== "-")
524
- .map((entry) => ({
525
- title: `未读 ${entry.title}`,
526
- detail: entry.detail
527
- }));
528
- const chatItems = chats.length > 0
529
- ? chats.map((chat) => chatItem(chat, userNames))
530
- : [{ title: "暂无最近私信", meta: "recent-contact-users" }];
531
- return {
532
- title: "消息",
533
- items: [...unreadItems, ...chatItems],
534
- status: "消息:j/k 选择 l 打开会话 h 返回 r 刷新"
535
- };
536
- }
537
- case "me": {
538
- const [me, cacheStats] = await Promise.all([
539
- client.getMe(force, signal),
540
- client.getCacheStats()
541
- ]);
542
- const meObject = asObject(me);
543
- return {
544
- title: "我的",
545
- items: [
546
- item("昵称", meObject.name),
547
- item("用户 ID", meObject.id),
548
- item("等级", meObject.levelTitle ?? meObject.groupName),
549
- item("发帖数", meObject.postCount),
550
- item("财富", meObject.wealth),
551
- item("关注", meObject.followCount),
552
- item("粉丝", meObject.fanCount),
553
- item("缓存文件", cacheStats.fileCacheEntries)
554
- ]
555
- };
556
- }
557
- case "settings": {
558
- return {
559
- title: "设置",
560
- items: settingsItems,
561
- status: "设置:j/k 选择 l 执行 h 返回"
562
- };
563
- }
564
- }
565
- }
566
- function currentTopicWidthEstimate(sidebarWidthOverride) {
567
- const width = process.stdout.columns || Number(process.env.COLUMNS) || 80;
568
- const sidebarWidth = getSidebarWidth(width, sidebarWidthOverride);
569
- const sidebarRuleWidth = sidebarWidth > 0 ? 1 : 0;
570
- return Math.max(24, width - sidebarWidth - sidebarRuleWidth);
571
- }
572
- function snapshotCurrentView(state) {
573
- if (state.mode === "topic" && state.topic) {
574
- return {
575
- kind: "topic",
576
- value: {
577
- viewTitle: state.viewTitle,
578
- status: state.status,
579
- scroll: state.scroll,
580
- topic: state.topic,
581
- list: snapshotCurrentList(state)
582
- }
583
- };
584
- }
585
- return {
586
- kind: "list",
587
- value: snapshotCurrentList(state)
588
- };
589
- }
590
- function snapshotCurrentList(state) {
591
- return {
592
- title: state.viewTitle,
593
- items: state.items,
594
- itemIndex: state.itemIndex,
595
- status: state.status,
596
- currentBoard: state.currentBoard,
597
- currentChat: state.currentChat,
598
- currentSearch: state.currentSearch,
599
- currentUser: state.currentUser
600
- };
601
- }
602
- function prepareListView(state, options) {
603
- if (options.pushParent) {
604
- state.history.push(snapshotCurrentView(state));
605
- if (state.history.length > state.historyLimit) {
606
- state.history = [];
607
- }
608
- }
609
- state.mode = "list";
610
- state.focus = "content";
611
- state.loading = true;
612
- state.loadingMore = false;
613
- state.error = undefined;
614
- state.itemIndex = 0;
615
- state.scroll = 0;
616
- state.topic = undefined;
617
- state.imageViewer = undefined;
618
- state.currentBoard = options.currentBoard;
619
- state.currentChat = options.currentChat;
620
- state.currentUser = options.currentUser;
621
- state.currentSearch = undefined;
622
- state.viewTitle = options.title;
623
- state.items = [];
624
- state.status = options.status;
625
- }
626
- function applyListSnapshot(state, snapshot) {
627
- state.mode = "list";
628
- state.focus = "content";
629
- state.loading = false;
630
- state.loadingMore = false;
631
- state.error = undefined;
632
- state.topic = undefined;
633
- state.imageViewer = undefined;
634
- state.currentBoard = snapshot.currentBoard;
635
- state.currentChat = snapshot.currentChat;
636
- state.currentSearch = snapshot.currentSearch;
637
- state.currentUser = snapshot.currentUser;
638
- state.viewTitle = snapshot.title;
639
- state.items = snapshot.items;
640
- state.itemIndex = snapshot.itemIndex;
641
- state.status = snapshot.status;
642
- }
643
- function applyTopicSnapshot(state, snapshot) {
644
- state.mode = "topic";
645
- state.focus = "content";
646
- state.loading = false;
647
- state.loadingMore = false;
648
- state.error = undefined;
649
- state.items = snapshot.list.items;
650
- state.itemIndex = snapshot.list.itemIndex;
651
- state.scroll = snapshot.scroll;
652
- state.topic = snapshot.topic;
653
- state.imageViewer = undefined;
654
- state.currentBoard = snapshot.list.currentBoard;
655
- state.currentChat = snapshot.list.currentChat;
656
- state.currentUser = snapshot.list.currentUser;
657
- state.currentSearch = snapshot.list.currentSearch;
658
- state.viewTitle = snapshot.viewTitle;
659
- state.status = snapshot.status;
660
- }
661
- async function loadTopicImagePreviews(state, render, config, sidebarWidthOverride) {
662
- const topic = state.topic;
663
- if (!topic || !config.previewImages) {
664
- return;
665
- }
666
- const width = Math.max(12, currentTopicWidthEstimate(sidebarWidthOverride) - 4);
667
- const imageLines = topic.posts
668
- .flatMap((post) => post.lines)
669
- .filter((line) => line.kind === "image" &&
670
- line.imageUrl);
671
- const previewEnabled = supportsImagePreview();
672
- for (const line of imageLines) {
673
- try {
674
- if (state.topic !== topic) {
675
- return;
676
- }
677
- const maxRows = isEmotionAssetPath(line.imageUrl ?? "") ? emotionPreviewRows : imagePreviewRows;
678
- if (!line.imagePreviewRows) {
679
- const measured = isEmotionAssetPath(line.imageUrl ?? "")
680
- ? await measureEmotionPreview(line.imageUrl ?? "", width)
681
- : await measureImagePreview(line.imageUrl ?? "", width, maxRows);
682
- if (state.topic !== topic) {
683
- return;
684
- }
685
- if (measured) {
686
- line.imagePreviewRows = measured.rows;
687
- adjustTopicImageBlockHeight(topic, line, measured.rows, state);
688
- render();
689
- }
690
- }
691
- if (!previewEnabled || line.imagePreview) {
692
- continue;
693
- }
694
- const rows = Math.max(1, Math.min(maxRows, line.imagePreviewRows ?? line.imageBlockRows ?? maxRows));
695
- const preview = isEmotionAssetPath(line.imageUrl ?? "")
696
- ? await loadEmotionPreview(line.imageUrl ?? "", width)
697
- : await loadImagePreview(line.imageUrl ?? "", width, rows);
698
- if (state.topic !== topic) {
699
- return;
700
- }
701
- if (preview) {
702
- line.imagePreview = preview.token;
703
- line.imagePreviewRows = preview.size.rows;
704
- adjustTopicImageBlockHeight(topic, line, preview.size.rows, state);
705
- render();
706
- }
707
- }
708
- catch {
709
- // Keep the textual image placeholder if preview loading fails.
710
- }
711
- }
712
- }
713
- function buildTopicReader(topicId, topic, posts, size, config) {
714
- const title = normalizeInlineText(String(topic.title ?? `#${topicId}`));
715
- const meta = [
716
- topic.userName,
717
- topic.replyCount !== undefined ? `${topic.replyCount} 回复` : undefined,
718
- topic.hitCount !== undefined ? `${topic.hitCount} 浏览` : undefined
719
- ].filter(Boolean).join(" · ");
720
- const rendered = renderPosts(posts, currentTopicWidthEstimate(), config);
721
- return {
722
- topicId,
723
- title,
724
- meta,
725
- lines: rendered.lines,
726
- posts: rendered.posts,
727
- loaded: posts.length,
728
- size,
729
- hasMore: posts.length === size,
730
- imageCount: rendered.imageCount,
731
- linkCount: rendered.linkCount,
732
- floorInput: ""
733
- };
734
- }
735
- function appendTopicPosts(topic, posts, config, sidebarWidthOverride) {
736
- const next = renderPosts(posts, Math.max(36, currentTopicWidthEstimate(sidebarWidthOverride)), config, topic.lines.length);
737
- topic.lines.push(...next.lines);
738
- topic.posts.push(...next.posts);
739
- topic.imageCount += next.imageCount;
740
- topic.linkCount += next.linkCount;
741
- }
742
- function renderPosts(posts, width, config, lineOffset = 0) {
743
- const lines = [];
744
- const entries = [];
745
- let imageCount = 0;
746
- let linkCount = 0;
747
- posts.forEach((postRaw) => {
748
- const post = asObject(postRaw);
749
- const lineStart = lineOffset + lines.length;
750
- const postLines = [];
751
- const floorNumber = asNumber(post.floor);
752
- const floor = floorNumber !== undefined ? `#${floorNumber}` : "#?";
753
- const author = String(post.userName ?? "匿名");
754
- const time = typeof post.time === "string" ? post.time.replace("T", " ").slice(0, 16) : "";
755
- const likeCount = asNumber(post.likeCount) ?? 0;
756
- const dislikeCount = asNumber(post.dislikeCount) ?? 0;
757
- const likeState = normalizeLikeState(post.likeState);
758
- const reactions = ` · ${likeCount} 赞 · ${dislikeCount} 踩`;
759
- const push = (text, kind, extra = {}) => {
760
- const line = lineOffset + lines.length;
761
- lines.push(text);
762
- postLines.push({
763
- line,
764
- row: postLines.length,
765
- floor: floorNumber,
766
- kind,
767
- text,
768
- ...extra
769
- });
770
- };
771
- push(`${floor} ${author}${time ? ` · ${time}` : ""}${reactions}`, "header");
772
- const contentWidth = Math.max(8, width - 2);
773
- push(theme.border.horizontal.repeat(contentWidth), "divider");
774
- const content = typeof post.content === "string" ? post.content : "";
775
- const contentType = asNumber(post.contentType) ?? 0;
776
- const rendered = contentType === 1
777
- ? renderMarkdownToLines(content, contentWidth, {
778
- imagePreviewRows: config.previewImages ? imagePreviewRows : 0
779
- })
780
- : renderUbbToLines(content, contentWidth, {
781
- imagePreviewRows: config.previewImages ? imagePreviewRows : 0
782
- });
783
- rendered.lines.forEach((renderedLine, index) => {
784
- const imageIndex = parseBracketIndex(renderedLine, "image");
785
- const linkIndex = parseBracketIndex(renderedLine, "link");
786
- const imageBlockRows = imageIndex !== undefined ? imageBlockHeight(rendered.lines, index) : undefined;
787
- const kind = renderedLine.trim() === ""
788
- ? "blank"
789
- : imageIndex !== undefined
790
- ? "image"
791
- : linkIndex !== undefined
792
- ? "link"
793
- : renderedLine.startsWith(theme.quote.prefix)
794
- ? "quote"
795
- : "text";
796
- push(renderedLine, kind, {
797
- imageIndex,
798
- imageUrl: imageIndex !== undefined ? rendered.images[imageIndex - 1] : undefined,
799
- imageBlockRows,
800
- linkIndex,
801
- linkUrl: linkIndex !== undefined ? rendered.links[linkIndex - 1] : undefined
802
- });
803
- });
804
- push("", "blank");
805
- const preview = rendered.lines.find((value) => value.trim() &&
806
- !value.startsWith("[image ") &&
807
- !value.startsWith("[link ")) ?? "";
808
- entries.push({
809
- id: asNumber(post.id),
810
- userId: asNumber(post.userId),
811
- floor: floorNumber,
812
- author,
813
- time,
814
- likeCount,
815
- dislikeCount,
816
- likeState,
817
- rating: formatRating(post),
818
- preview,
819
- lineStart,
820
- lineEnd: lineOffset + lines.length - 1,
821
- imageCount: rendered.images.length,
822
- linkCount: rendered.links.length,
823
- images: rendered.images,
824
- links: rendered.links,
825
- lines: postLines
826
- });
827
- imageCount += rendered.images.length;
828
- linkCount += rendered.links.length;
829
- });
830
- return { lines, posts: entries, imageCount, linkCount };
831
- }
832
- function normalizeLikeState(value) {
833
- return value === 1 || value === 2 ? value : 0;
834
- }
835
- function findTopicPostByFloor(topic, floor) {
836
- return topic.posts.find((entry) => entry.floor === floor);
837
- }
838
- function imageBlockHeight(lines, start) {
839
- let height = 1;
840
- for (let index = start + 1; index < lines.length; index += 1) {
841
- const line = lines[index] ?? "";
842
- if (line.startsWith("[image ") || line.trim() !== "") {
843
- break;
844
- }
845
- height += 1;
846
- }
847
- return height;
848
- }
849
- function adjustTopicImageBlockHeight(topic, line, nextRows, state) {
850
- const post = topic.posts.find((entry) => entry.lines.includes(line));
851
- if (!post) {
852
- return;
853
- }
854
- const startRow = line.row;
855
- const currentRows = Math.max(1, line.imageBlockRows ?? 1);
856
- const targetRows = Math.max(1, nextRows);
857
- if (currentRows === targetRows) {
858
- line.imageBlockRows = targetRows;
859
- return;
860
- }
861
- const originalLine = line.line;
862
- const removeCount = Math.min(currentRows, Math.max(1, post.lines.length - startRow));
863
- const delta = targetRows - currentRows;
864
- const filler = Array.from({ length: targetRows - 1 }, () => ({
865
- line: 0,
866
- row: 0,
867
- floor: line.floor,
868
- kind: "blank",
869
- text: ""
870
- }));
871
- line.imageBlockRows = targetRows;
872
- post.lines.splice(startRow, removeCount, line, ...filler);
873
- rebuildTopicLines(topic);
874
- if (originalLine < state.scroll) {
875
- state.scroll = Math.max(0, state.scroll + delta);
876
- }
877
- }
878
- function rebuildTopicLines(topic) {
879
- const lines = [];
880
- topic.posts.forEach((post) => {
881
- post.lineStart = lines.length;
882
- post.lines.forEach((entry, row) => {
883
- entry.row = row;
884
- entry.line = lines.length;
885
- lines.push(entry.text);
886
- });
887
- post.lineEnd = Math.max(post.lineStart, lines.length - 1);
888
- });
889
- topic.lines = lines;
890
- }
891
- function parseBracketIndex(value, label) {
892
- const match = new RegExp(`\\[${label} (\\d+)`).exec(value);
893
- return match ? Number(match[1]) : undefined;
894
- }
895
- function formatRating(post) {
896
- const value = post.rating ?? post.ratingCount ?? post.wealth ?? post.score;
897
- if (typeof value === "number" && Number.isFinite(value)) {
898
- return String(value);
899
- }
900
- if (typeof value === "string" && value.trim()) {
901
- return value.trim();
902
- }
903
- return undefined;
904
- }
905
- function item(title, value, meta) {
906
- return {
907
- title,
908
- meta,
909
- detail: value === undefined || value === null ? "-" : String(value)
910
- };
911
- }
912
- function buildUserProfileItems(profile) {
913
- const userId = asNumber(profile.id);
914
- const name = String(profile.name ?? profile.userName ?? (userId !== undefined ? `#${userId}` : "用户")).trim() || "用户";
915
- const group = String(profile.displayTitle ?? profile.privilege ?? profile.levelTitle ?? "").trim();
916
- const introduction = normalizePreview(String(profile.introduction ?? ""));
917
- return [
918
- {
919
- title: name,
920
- meta: [userId !== undefined ? `#${userId}` : undefined, group || undefined].filter(Boolean).join(" · "),
921
- detail: introduction || "这个用户没有留下简介。"
922
- },
923
- item("发帖数", profile.postCount),
924
- item("粉丝数", profile.fanCount),
925
- item("关注数", profile.followCount),
926
- item("威望", profile.prestige),
927
- item("财富值", profile.wealth),
928
- item("风评", profile.popularity),
929
- item("收到的赞", profile.receivedLikeCount),
930
- item("注册时间", formatTime(profile.registerTime)),
931
- item("最后登录", formatTime(profile.lastLogOnTime))
932
- ];
933
- }
934
- function describeUserProfileStatus(state) {
935
- return state.currentUser?.hasMore
936
- ? "用户页:j/k 浏览 l 打开主题 n/Space 更多主题 Esc/Backspace 返回"
937
- : "用户页:j/k 浏览 l 打开主题 Esc/Backspace 返回";
938
- }
939
- function topicItem(value, fallbackBoard) {
940
- const topic = asObject(value);
941
- const topicId = asNumber(topic.id ?? topic.Id);
942
- const boardId = asNumber(topic.boardId ?? topic.BoardId) ?? fallbackBoard?.boardId;
943
- const boardName = topic.boardName ?? topic.BoardName ?? fallbackBoard?.title;
944
- const authorName = normalizeInlineText(String(topic.userName ?? topic.authorName ?? "")).trim() || "匿名";
945
- return {
946
- title: normalizeInlineText(String(topic.title ?? topic.Title ?? `#${topicId ?? ""}`)),
947
- meta: [
948
- boardName,
949
- authorName,
950
- topic.replyCount !== undefined ? `${topic.replyCount} 回复` : undefined,
951
- topic.hitCount !== undefined ? `${topic.hitCount} 浏览` : undefined
952
- ]
953
- .filter(Boolean)
954
- .join(" · "),
955
- detail: typeof topic.lastPostContent === "string" ? topic.lastPostContent.replace(/\s+/g, " ") : undefined,
956
- topicId,
957
- boardId,
958
- sortTime: timestampOf(topic.lastPostTime ?? topic.updateTime ?? topic.time ?? topic.createTime)
959
- };
960
- }
961
- async function loadChatUserNames(client, chats, force, signal) {
962
- const ids = chats
963
- .map((chat) => asNumber(asObject(chat).userId ?? asObject(chat).UserId))
964
- .filter((id) => id !== undefined);
965
- const users = asArray(await client.getBasicUsers(ids, force, signal));
966
- return new Map(users.map((userRaw) => {
967
- const user = asObject(userRaw);
968
- const id = asNumber(user.id ?? user.Id);
969
- const name = String(user.name ?? user.Name ?? (id !== undefined ? `#${id}` : "用户"));
970
- return [id, name];
971
- }).filter((entry) => entry[0] !== undefined));
972
- }
973
- function chatItem(value, userNames) {
974
- const chat = asObject(value);
975
- const userId = asNumber(chat.userId ?? chat.UserId);
976
- const name = userId !== undefined ? userNames.get(userId) : undefined;
977
- return {
978
- title: String(name ?? chat.name ?? chat.userName ?? userId ?? "私信"),
979
- meta: userId !== undefined ? `user #${userId}` : undefined,
980
- detail: normalizePreview(String(chat.lastContent ?? chat.lastMessage ?? chat.content ?? "")),
981
- chatUserId: userId
982
- };
983
- }
984
- function chatMessageItems(messages, otherName, otherUserId) {
985
- return [...messages].reverse().map((messageRaw) => {
986
- const message = asObject(messageRaw);
987
- const receiverId = asNumber(message.receiverId ?? message.ReceiverId);
988
- const isMine = receiverId === otherUserId;
989
- const time = typeof message.time === "string"
990
- ? message.time.replace("T", " ").slice(0, 16)
991
- : "";
992
- const content = normalizePreview(String(message.content ?? message.Content ?? ""));
993
- return {
994
- title: isMine ? `我 -> ${otherName}` : `${otherName} -> 我`,
995
- meta: [time, receiverId !== undefined ? `receiver #${receiverId}` : undefined].filter(Boolean).join(" · "),
996
- detail: content || "(空消息)"
997
- };
998
- });
999
- }
1000
- function unreadStats(value) {
1001
- return [
1002
- item("系统", value.systemCount),
1003
- item("@", value.atCount),
1004
- item("回复", value.replyCount),
1005
- item("私信", value.messageCount)
1006
- ];
1007
- }
1008
- function overviewStats(index, unread) {
1009
- const unreadTotal = ["systemCount", "atCount", "replyCount", "messageCount"].reduce((total, key) => {
1010
- const value = unread[key];
1011
- return total + (typeof value === "number" ? value : 0);
1012
- }, 0);
1013
- return [
1014
- item("今日主题", index.todayTopicCount),
1015
- item("今日回复", index.todayCount),
1016
- item("在线", index.onlineUserCount),
1017
- item("用户", index.userCount),
1018
- item("未读", unreadTotal)
1019
- ];
1020
- }
1021
- async function mapLimit(values, limit, mapper) {
1022
- const results = [];
1023
- let nextIndex = 0;
1024
- const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
1025
- while (nextIndex < values.length) {
1026
- const index = nextIndex;
1027
- nextIndex += 1;
1028
- results[index] = await mapper(values[index]);
1029
- }
1030
- });
1031
- await Promise.all(workers);
1032
- return results;
1033
- }
1034
- function flattenBoards(sections) {
1035
- const boards = [];
1036
- for (const section of sections) {
1037
- const sectionObject = asObject(section);
1038
- const sectionName = String(sectionObject.name ?? sectionObject.title ?? "分区");
1039
- const candidates = [sectionObject.boards, sectionObject.children, sectionObject.boardList];
1040
- for (const candidate of candidates) {
1041
- if (!Array.isArray(candidate)) {
1042
- continue;
1043
- }
1044
- for (const board of candidate) {
1045
- const boardObject = asObject(board);
1046
- boards.push({
1047
- title: String(boardObject.name ?? boardObject.title ?? `#${boardObject.id ?? ""}`),
1048
- meta: `${sectionName}${boardObject.id !== undefined ? ` · #${boardObject.id}` : ""}`,
1049
- detail: typeof boardObject.description === "string" ? boardObject.description : undefined,
1050
- boardId: typeof boardObject.id === "number" ? boardObject.id : undefined
1051
- });
1052
- }
1053
- }
1054
- }
1055
- return boards;
1056
- }
1057
- function asObject(value) {
1058
- return typeof value === "object" && value !== null ? value : {};
1059
- }
1060
- function asArray(value) {
1061
- return Array.isArray(value) ? value : [];
1062
- }
1063
- function asNumber(value) {
1064
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1065
- }
1066
- function normalizeInline(value) {
1067
- return value.replace(/\s+/g, " ").trim();
1068
- }
1069
- function normalizePreview(value) {
1070
- return normalizeInline(value
1071
- .replace(/\[img\][\s\S]*?\[\/img\]/gi, " [图片] ")
1072
- .replace(/\[upload(?:=[^\]]*)?\][\s\S]*?\[\/upload\]/gi, " [附件] ")
1073
- .replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi, (_match, _url, label) => ` ${label} `)
1074
- .replace(/\[url\][\s\S]*?\[\/url\]/gi, " [链接] ")
1075
- .replace(/<img\b[^>]*>/gi, " [图片] ")
1076
- .replace(/<br\s*\/?>/gi, " ")
1077
- .replace(/<\/?[^>]+>/g, " ")
1078
- .replace(/\[(?:\/)?(?:b|i|u|size|color|align|email|del|s|sub|sup|h\d?|quote|code)(?:=[^\]]*)?\]/gi, "")
1079
- .replace(/\[[a-z0-9]+(?:=[^\]]*)?\]/gi, " ")
1080
- .replace(/\[\/[a-z0-9]+\]/gi, " "));
1081
- }
1082
- function timestampOf(value) {
1083
- if (typeof value !== "string" && typeof value !== "number") {
1084
- return undefined;
1085
- }
1086
- const timestamp = new Date(value).getTime();
1087
- return Number.isFinite(timestamp) ? timestamp : undefined;
1088
- }
1089
- function formatTime(value) {
1090
- if (typeof value !== "string" || !value.trim()) {
1091
- return "-";
1092
- }
1093
- return value.replace("T", " ").slice(0, 16);
1094
- }
1095
- function normalizeInlineText(value) {
1096
- return value.replace(/\s+/g, " ").trim();
1097
- }
1098
- function isAbortError(error) {
1099
- return error instanceof Error && error.name === "AbortError";
1100
- }
1
+ export { getDefaultAccountName, normalizeLoginMessage, refreshAccounts } from "./data/accounts.js";
2
+ export { loadNextChatPage, loadNextFeedPage, loadNextUserTopicPage, openBoard, openChat, openUserProfile } from "./data/content.js";
3
+ export { createSearchState, prepareListView, restorePreviousView } from "./data/navigation-state.js";
4
+ export { executeSearch, loadNextSearchPage } from "./data/search.js";
5
+ export { jumpRelativeTopicFloor, jumpToTopicFloor, loadNextTopicPage, openTopic } from "./data/topic.js";
6
+ export { describeUserProfileStatus } from "./data/view-items.js";
7
+ export { loadView } from "./data/view-loader.js";
1101
8
  //# sourceMappingURL=app-data.js.map