cc98-cli 0.2.0 → 0.4.0

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 (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +4 -2
  3. package/dist/api/client.d.ts +40 -15
  4. package/dist/api/client.d.ts.map +1 -1
  5. package/dist/api/client.js +179 -38
  6. package/dist/api/client.js.map +1 -1
  7. package/dist/api/endpoints.d.ts +14 -0
  8. package/dist/api/endpoints.d.ts.map +1 -1
  9. package/dist/api/endpoints.js +21 -1
  10. package/dist/api/endpoints.js.map +1 -1
  11. package/dist/api/types.d.ts +13 -0
  12. package/dist/api/types.d.ts.map +1 -1
  13. package/dist/api/webvpn.d.ts +68 -0
  14. package/dist/api/webvpn.d.ts.map +1 -0
  15. package/dist/api/webvpn.js +311 -0
  16. package/dist/api/webvpn.js.map +1 -0
  17. package/dist/cli/commands/board.d.ts.map +1 -1
  18. package/dist/cli/commands/board.js +16 -1
  19. package/dist/cli/commands/board.js.map +1 -1
  20. package/dist/cli/commands/forum.js +1 -1
  21. package/dist/cli/commands/forum.js.map +1 -1
  22. package/dist/cli/commands/login.js +1 -1
  23. package/dist/cli/commands/login.js.map +1 -1
  24. package/dist/cli/commands/logout.js +2 -2
  25. package/dist/cli/commands/logout.js.map +1 -1
  26. package/dist/cli/commands/me.d.ts.map +1 -1
  27. package/dist/cli/commands/me.js +18 -3
  28. package/dist/cli/commands/me.js.map +1 -1
  29. package/dist/cli/commands/message.d.ts.map +1 -1
  30. package/dist/cli/commands/message.js +11 -1
  31. package/dist/cli/commands/message.js.map +1 -1
  32. package/dist/cli/commands/notice.js +1 -1
  33. package/dist/cli/commands/notice.js.map +1 -1
  34. package/dist/cli/commands/post.d.ts.map +1 -1
  35. package/dist/cli/commands/post.js +13 -1
  36. package/dist/cli/commands/post.js.map +1 -1
  37. package/dist/cli/commands/search.js +1 -1
  38. package/dist/cli/commands/search.js.map +1 -1
  39. package/dist/cli/commands/topic.d.ts.map +1 -1
  40. package/dist/cli/commands/topic.js +42 -6
  41. package/dist/cli/commands/topic.js.map +1 -1
  42. package/dist/cli/commands/user.d.ts.map +1 -1
  43. package/dist/cli/commands/user.js +13 -1
  44. package/dist/cli/commands/user.js.map +1 -1
  45. package/dist/cli/commands/vpn.d.ts +2 -0
  46. package/dist/cli/commands/vpn.d.ts.map +1 -0
  47. package/dist/cli/commands/vpn.js +200 -0
  48. package/dist/cli/commands/vpn.js.map +1 -0
  49. package/dist/cli/context.d.ts +2 -2
  50. package/dist/cli/context.d.ts.map +1 -1
  51. package/dist/cli/context.js +20 -2
  52. package/dist/cli/context.js.map +1 -1
  53. package/dist/cli/router.d.ts.map +1 -1
  54. package/dist/cli/router.js +13 -2
  55. package/dist/cli/router.js.map +1 -1
  56. package/dist/main.js +0 -0
  57. package/dist/storage/image-cache.d.ts +32 -0
  58. package/dist/storage/image-cache.d.ts.map +1 -0
  59. package/dist/storage/image-cache.js +90 -0
  60. package/dist/storage/image-cache.js.map +1 -0
  61. package/dist/storage/vpn-store.d.ts +39 -0
  62. package/dist/storage/vpn-store.d.ts.map +1 -0
  63. package/dist/storage/vpn-store.js +94 -0
  64. package/dist/storage/vpn-store.js.map +1 -0
  65. package/dist/tui/app.d.ts.map +1 -1
  66. package/dist/tui/app.js +27 -1725
  67. package/dist/tui/app.js.map +1 -1
  68. package/dist/tui/borders.d.ts +183 -0
  69. package/dist/tui/borders.d.ts.map +1 -0
  70. package/dist/tui/borders.js +143 -0
  71. package/dist/tui/borders.js.map +1 -0
  72. package/dist/tui/cached-client.d.ts +23 -0
  73. package/dist/tui/cached-client.d.ts.map +1 -1
  74. package/dist/tui/cached-client.js +88 -0
  75. package/dist/tui/cached-client.js.map +1 -1
  76. package/dist/tui/components/content.d.ts +11 -0
  77. package/dist/tui/components/content.d.ts.map +1 -0
  78. package/dist/tui/components/content.js +129 -0
  79. package/dist/tui/components/content.js.map +1 -0
  80. package/dist/tui/components/header.d.ts +7 -0
  81. package/dist/tui/components/header.d.ts.map +1 -0
  82. package/dist/tui/components/header.js +38 -0
  83. package/dist/tui/components/header.js.map +1 -0
  84. package/dist/tui/components/index.d.ts +8 -0
  85. package/dist/tui/components/index.d.ts.map +1 -0
  86. package/dist/tui/components/index.js +9 -0
  87. package/dist/tui/components/index.js.map +1 -0
  88. package/dist/tui/components/layout.d.ts +3 -0
  89. package/dist/tui/components/layout.d.ts.map +1 -0
  90. package/dist/tui/components/layout.js +453 -0
  91. package/dist/tui/components/layout.js.map +1 -0
  92. package/dist/tui/components/overview.d.ts +7 -0
  93. package/dist/tui/components/overview.d.ts.map +1 -0
  94. package/dist/tui/components/overview.js +30 -0
  95. package/dist/tui/components/overview.js.map +1 -0
  96. package/dist/tui/components/sidebar.d.ts +7 -0
  97. package/dist/tui/components/sidebar.d.ts.map +1 -0
  98. package/dist/tui/components/sidebar.js +57 -0
  99. package/dist/tui/components/sidebar.js.map +1 -0
  100. package/dist/tui/components/status.d.ts +8 -0
  101. package/dist/tui/components/status.d.ts.map +1 -0
  102. package/dist/tui/components/status.js +42 -0
  103. package/dist/tui/components/status.js.map +1 -0
  104. package/dist/tui/components/types.d.ts +17 -0
  105. package/dist/tui/components/types.d.ts.map +1 -0
  106. package/dist/tui/components/types.js +3 -0
  107. package/dist/tui/components/types.js.map +1 -0
  108. package/dist/tui/components/utils.d.ts +5 -0
  109. package/dist/tui/components/utils.d.ts.map +1 -0
  110. package/dist/tui/components/utils.js +68 -0
  111. package/dist/tui/components/utils.js.map +1 -0
  112. package/dist/tui/controller.d.ts +66 -0
  113. package/dist/tui/controller.d.ts.map +1 -0
  114. package/dist/tui/controller.js +1200 -0
  115. package/dist/tui/controller.js.map +1 -0
  116. package/dist/tui/helpers.d.ts +25 -0
  117. package/dist/tui/helpers.d.ts.map +1 -0
  118. package/dist/tui/helpers.js +249 -0
  119. package/dist/tui/helpers.js.map +1 -0
  120. package/dist/tui/navigation.d.ts +4 -0
  121. package/dist/tui/navigation.d.ts.map +1 -0
  122. package/dist/tui/navigation.js +19 -0
  123. package/dist/tui/navigation.js.map +1 -0
  124. package/dist/tui/renderer.d.ts +6 -0
  125. package/dist/tui/renderer.d.ts.map +1 -0
  126. package/dist/tui/renderer.js +305 -0
  127. package/dist/tui/renderer.js.map +1 -0
  128. package/dist/tui/state/index.d.ts +3 -0
  129. package/dist/tui/state/index.d.ts.map +1 -0
  130. package/dist/tui/state/index.js +4 -0
  131. package/dist/tui/state/index.js.map +1 -0
  132. package/dist/tui/state/store.d.ts +4 -0
  133. package/dist/tui/state/store.d.ts.map +1 -0
  134. package/dist/tui/state/store.js +62 -0
  135. package/dist/tui/state/store.js.map +1 -0
  136. package/dist/tui/state/types.d.ts +141 -0
  137. package/dist/tui/state/types.d.ts.map +1 -0
  138. package/dist/tui/state/types.js +3 -0
  139. package/dist/tui/state/types.js.map +1 -0
  140. package/dist/tui/topic-reader.d.ts +10 -0
  141. package/dist/tui/topic-reader.d.ts.map +1 -0
  142. package/dist/tui/topic-reader.js +178 -0
  143. package/dist/tui/topic-reader.js.map +1 -0
  144. package/dist/tui/ubb-renderer.d.ts.map +1 -1
  145. package/dist/tui/ubb-renderer.js +176 -12
  146. package/dist/tui/ubb-renderer.js.map +1 -1
  147. package/dist/version.d.ts +1 -1
  148. package/dist/version.js +1 -1
  149. package/package.json +2 -2
  150. /package/{docs/images → images}/tui.jpg +0 -0
@@ -0,0 +1,1200 @@
1
+ import { checkForUpdate } from "../update.js";
2
+ import { appVersion } from "../version.js";
3
+ import { getImageCache } from "../storage/image-cache.js";
4
+ import { navItems, settingsItems } from "./navigation.js";
5
+ import { getStatus } from "./state/store.js";
6
+ import { asArray, asNumber, asObject, chatItem, chatMessageItems, flattenBoards, genericItem, historyItem, isAbortError, jsonPreviewLines, loadChatUserNames, mapLimit, noticeItem, overviewStats, topicItem, unreadStats, userItem } from "./helpers.js";
7
+ import { appendTopicPosts, buildTopicReader, currentTopicPost, findTopicPostByFloor, jumpRelativeTopicFloor } from "./topic-reader.js";
8
+ export class TuiController {
9
+ state;
10
+ client;
11
+ tokenStore;
12
+ render;
13
+ close;
14
+ nextSignal;
15
+ abortCurrent;
16
+ loadVersion = 0;
17
+ constructor(state, client, tokenStore, render, close, nextSignal, abortCurrent) {
18
+ this.state = state;
19
+ this.client = client;
20
+ this.tokenStore = tokenStore;
21
+ this.render = render;
22
+ this.close = close;
23
+ this.nextSignal = nextSignal;
24
+ this.abortCurrent = abortCurrent;
25
+ }
26
+ async load(force = false) {
27
+ const version = ++this.loadVersion;
28
+ const signal = this.nextSignal();
29
+ const nav = navItems[this.state.navIndex] ?? navItems[0];
30
+ this.state.viewTitle = nav.label;
31
+ this.state.loading = true;
32
+ this.state.error = undefined;
33
+ this.state.itemIndex = 0;
34
+ this.state.scroll = 0;
35
+ this.state.mode = nav.id === "settings" && this.state.mode === "settings" ? "settings" : "list";
36
+ if (this.state.mode === "settings") {
37
+ this.state.focus = "content";
38
+ }
39
+ this.state.items = [];
40
+ this.state.stats = [];
41
+ this.state.topic = undefined;
42
+ this.state.parentList = undefined;
43
+ this.state.currentBoard = undefined;
44
+ this.state.currentChat = undefined;
45
+ this.render();
46
+ try {
47
+ this.state.account = await this.tokenStore.getCurrentAccountName();
48
+ const next = await this.loadView(nav.id, force, signal);
49
+ if (version !== this.loadVersion)
50
+ return;
51
+ this.state.viewTitle = next.title;
52
+ this.state.items = next.items;
53
+ this.state.stats = next.stats;
54
+ if (next.overview) {
55
+ this.state.overview = next.overview;
56
+ }
57
+ this.state.status = next.status ?? "";
58
+ }
59
+ catch (error) {
60
+ if (isAbortError(error) || version !== this.loadVersion)
61
+ return;
62
+ this.state.error = error instanceof Error ? error.message : String(error);
63
+ this.state.items = [];
64
+ this.state.stats = [];
65
+ }
66
+ finally {
67
+ if (version === this.loadVersion) {
68
+ this.state.loading = false;
69
+ this.render();
70
+ }
71
+ }
72
+ }
73
+ handleKey(key) {
74
+ if (this.state.inputMode) {
75
+ this.handleInputKey(key);
76
+ return;
77
+ }
78
+ if (key === "\u0003" || key === "q") {
79
+ this.close();
80
+ return;
81
+ }
82
+ if (key === "?") {
83
+ this.state.modal = this.state.modal === "help" ? null : "help";
84
+ this.render();
85
+ return;
86
+ }
87
+ if (this.state.modal) {
88
+ this.handleModalKey(key);
89
+ return;
90
+ }
91
+ if (this.state.mode === "topic") {
92
+ this.handleTopicKey(key);
93
+ return;
94
+ }
95
+ if (this.state.mode === "settings") {
96
+ this.handleSettingsKey(key);
97
+ return;
98
+ }
99
+ if (this.state.focus === "nav") {
100
+ this.handleNavKey(key);
101
+ return;
102
+ }
103
+ this.handleContentKey(key);
104
+ }
105
+ handleInputKey(key) {
106
+ if (key === "\x1b") {
107
+ this.state.inputMode = false;
108
+ this.state.inputValue = "";
109
+ this.render();
110
+ return;
111
+ }
112
+ if (key === "\r") {
113
+ this.state.inputCallback?.(this.state.inputValue);
114
+ return;
115
+ }
116
+ if (key === "\x7f") {
117
+ this.state.inputValue = this.state.inputValue.slice(0, -1);
118
+ this.render();
119
+ return;
120
+ }
121
+ if (key.length === 1 && key >= " ") {
122
+ this.state.inputValue += key;
123
+ this.render();
124
+ }
125
+ }
126
+ handleModalKey(key) {
127
+ if (this.state.modal === "help" || this.state.modal === "info") {
128
+ // 帮助/信息弹窗:任意键关闭
129
+ this.closeModal();
130
+ return;
131
+ }
132
+ if (this.state.modal === "search") {
133
+ this.handleSearchKey(key);
134
+ return;
135
+ }
136
+ if (this.state.modal === "user") {
137
+ this.handleUserModalKey(key);
138
+ return;
139
+ }
140
+ if (this.state.modal === "menu") {
141
+ this.handleMenuKey(key);
142
+ }
143
+ }
144
+ handleSearchKey(key) {
145
+ if (key === "\x1b" || key === "/") {
146
+ // Esc 或 / 关闭搜索
147
+ this.state.modal = null;
148
+ this.state.searchQuery = "";
149
+ this.render();
150
+ return;
151
+ }
152
+ if (key === "\t") {
153
+ this.state.searchMode = this.state.searchMode === "topics" ? "users" : "topics";
154
+ this.state.searchResults = [];
155
+ this.state.itemIndex = 0;
156
+ this.render();
157
+ return;
158
+ }
159
+ if ((key === "j" || key === "\x1b[B") && this.state.searchResults.length > 0) {
160
+ this.state.itemIndex = Math.min(this.state.searchResults.length - 1, this.state.itemIndex + 1);
161
+ this.render();
162
+ return;
163
+ }
164
+ if ((key === "k" || key === "\x1b[A") && this.state.searchResults.length > 0) {
165
+ this.state.itemIndex = Math.max(0, this.state.itemIndex - 1);
166
+ this.render();
167
+ return;
168
+ }
169
+ if (key === "\r") {
170
+ const selected = this.state.searchResults[this.state.itemIndex];
171
+ if (selected) {
172
+ // 有选中项:打开
173
+ this.state.modal = null;
174
+ void this.activateContentItem(selected, this.nextSignal());
175
+ }
176
+ else if (this.state.searchQuery.trim()) {
177
+ // 无选中项:执行搜索
178
+ void this.performSearch(this.nextSignal());
179
+ }
180
+ return;
181
+ }
182
+ if (key === "\x7f") {
183
+ this.state.searchQuery = this.state.searchQuery.slice(0, -1);
184
+ this.state.searchResults = [];
185
+ this.state.itemIndex = 0;
186
+ this.render();
187
+ return;
188
+ }
189
+ if (key.length === 1 && key >= " ") {
190
+ this.state.searchQuery += key;
191
+ this.state.searchResults = [];
192
+ this.state.itemIndex = 0;
193
+ this.render();
194
+ }
195
+ }
196
+ handleUserModalKey(key) {
197
+ if (key === "\x1b" || key === "u") {
198
+ // Esc 或 u 关闭用户详情
199
+ this.closeModal();
200
+ return;
201
+ }
202
+ if (key === "f" && this.state.userDetail) {
203
+ void this.toggleFollow();
204
+ return;
205
+ }
206
+ if (key === "m" && this.state.userDetail) {
207
+ const user = this.state.userDetail;
208
+ this.state.inputMode = true;
209
+ this.state.inputPrompt = `发送私信给 ${user.name}: `;
210
+ this.state.inputValue = "";
211
+ this.state.inputCallback = (value) => {
212
+ this.state.inputMode = false;
213
+ this.state.inputValue = "";
214
+ if (value.trim()) {
215
+ void this.sendPrivateMessage(user.userId, value.trim());
216
+ }
217
+ else {
218
+ this.render();
219
+ }
220
+ };
221
+ this.render();
222
+ }
223
+ }
224
+ handleMenuKey(key) {
225
+ if (key === "j" || key === "\x1b[B") {
226
+ this.state.menuIndex = Math.min(Math.max(0, this.state.menuItems.length - 1), this.state.menuIndex + 1);
227
+ this.render();
228
+ return;
229
+ }
230
+ if (key === "k" || key === "\x1b[A") {
231
+ this.state.menuIndex = Math.max(0, this.state.menuIndex - 1);
232
+ this.render();
233
+ return;
234
+ }
235
+ if (key === "\x1b" || key === "o") {
236
+ // Esc 或 o 关闭菜单
237
+ this.state.modal = null;
238
+ this.render();
239
+ return;
240
+ }
241
+ if (key === "\r" || key === "l") {
242
+ // Enter 或 l 执行选中项
243
+ const selected = this.state.menuItems[this.state.menuIndex];
244
+ this.state.modal = null;
245
+ if (selected?.action === "refresh")
246
+ void this.refresh();
247
+ if (selected?.action === "back")
248
+ this.leave();
249
+ this.render();
250
+ }
251
+ }
252
+ handleTopicKey(key) {
253
+ if (/^\d$/.test(key) && this.state.topic) {
254
+ this.state.topic.floorInput = `${this.state.topic.floorInput}${key}`.slice(0, 6);
255
+ this.state.status = `跳转到 ${this.state.topic.floorInput} 楼:Enter 确认 Esc 取消`;
256
+ this.render();
257
+ return;
258
+ }
259
+ if (key === "\x7f" && this.state.topic?.floorInput) {
260
+ this.state.topic.floorInput = this.state.topic.floorInput.slice(0, -1);
261
+ this.state.status = this.state.topic.floorInput ? `跳转到 ${this.state.topic.floorInput} 楼:Enter 确认 Esc 取消` : getStatus(this.state);
262
+ this.render();
263
+ return;
264
+ }
265
+ if (key === "\r" && this.state.topic?.floorInput) {
266
+ const floor = Number(this.state.topic.floorInput);
267
+ this.state.topic.floorInput = "";
268
+ if (Number.isInteger(floor) && floor > 0)
269
+ void this.jumpToTopicFloor(floor, this.nextSignal());
270
+ return;
271
+ }
272
+ if ((key === "]" || key === "】") && this.state.topic) {
273
+ this.state.scroll = jumpRelativeTopicFloor(this.state.topic, this.state.scroll, 1);
274
+ this.state.status = getStatus(this.state);
275
+ this.render();
276
+ return;
277
+ }
278
+ if ((key === "[" || key === "【") && this.state.topic) {
279
+ this.state.scroll = jumpRelativeTopicFloor(this.state.topic, this.state.scroll, -1);
280
+ this.state.status = getStatus(this.state);
281
+ this.render();
282
+ return;
283
+ }
284
+ if (key === "\x1b" && this.state.topic?.floorInput) {
285
+ this.state.topic.floorInput = "";
286
+ this.state.status = getStatus(this.state);
287
+ this.render();
288
+ return;
289
+ }
290
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
291
+ this.leave();
292
+ return;
293
+ }
294
+ if (key === "j" || key === "\x1b[B") {
295
+ const maxScroll = Math.max(0, (this.state.topic?.lines.length ?? 0) - 1);
296
+ const wasAtEnd = this.state.scroll >= maxScroll;
297
+ this.state.scroll = Math.min(maxScroll, this.state.scroll + 1);
298
+ this.render();
299
+ if (wasAtEnd && this.state.topic?.hasMore && !this.state.loadingMore) {
300
+ void this.loadNextTopicPage(this.nextSignal(), true);
301
+ }
302
+ return;
303
+ }
304
+ if (key === "k" || key === "\x1b[A") {
305
+ this.state.scroll = Math.max(0, this.state.scroll - 1);
306
+ this.render();
307
+ return;
308
+ }
309
+ if (key === "n" || key === " ") {
310
+ void this.loadNextTopicPage(this.nextSignal());
311
+ return;
312
+ }
313
+ if (key === "r" && this.state.topic) {
314
+ void this.openTopic(this.state.topic.topicId, true, this.nextSignal());
315
+ return;
316
+ }
317
+ if (key === "s")
318
+ void this.toggleFavorite();
319
+ if (key === "l")
320
+ void this.reactToCurrentPost(true);
321
+ if (key === "d")
322
+ void this.reactToCurrentPost(false);
323
+ if (key === "u")
324
+ void this.showCurrentUser(this.nextSignal());
325
+ if (key === "v")
326
+ void this.showTopicVote(this.nextSignal());
327
+ if (key === "a")
328
+ void this.showPostReactionState(this.nextSignal());
329
+ if (key === "o") {
330
+ // 如果当前行是图片行,打开图片;否则打开菜单
331
+ const currentLine = this.getCurrentTopicLine();
332
+ if (currentLine?.kind === "image" && currentLine.imageUrl) {
333
+ void this.openImage(currentLine.imageUrl);
334
+ }
335
+ else {
336
+ this.openMenu();
337
+ }
338
+ }
339
+ if (key === "c") {
340
+ // 复制图片链接
341
+ const currentLine = this.getCurrentTopicLine();
342
+ if (currentLine?.kind === "image" && currentLine.imageUrl) {
343
+ void this.copyToClipboard(currentLine.imageUrl);
344
+ }
345
+ else if (currentLine?.kind === "link" && currentLine.linkUrl) {
346
+ void this.copyToClipboard(currentLine.linkUrl);
347
+ }
348
+ }
349
+ }
350
+ handleSettingsKey(key) {
351
+ if (key === "j" || key === "\x1b[B") {
352
+ this.state.itemIndex = Math.min(settingsItems.length - 1, this.state.itemIndex + 1);
353
+ this.render();
354
+ return;
355
+ }
356
+ if (key === "k" || key === "\x1b[A") {
357
+ this.state.itemIndex = Math.max(0, this.state.itemIndex - 1);
358
+ this.render();
359
+ return;
360
+ }
361
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
362
+ this.state.mode = "list";
363
+ this.state.focus = "nav";
364
+ this.state.status = getStatus(this.state);
365
+ this.render();
366
+ return;
367
+ }
368
+ if (key === "l" || key === "\x1b[C" || key === "\r") {
369
+ void this.activateSetting(settingsItems[this.state.itemIndex]);
370
+ }
371
+ }
372
+ handleNavKey(key) {
373
+ if (key === "j" || key === "\x1b[B") {
374
+ this.state.navIndex = Math.min(navItems.length - 1, this.state.navIndex + 1);
375
+ void this.load();
376
+ return;
377
+ }
378
+ if (key === "k" || key === "\x1b[A") {
379
+ this.state.navIndex = Math.max(0, this.state.navIndex - 1);
380
+ void this.load();
381
+ return;
382
+ }
383
+ if (key === "l" || key === "\x1b[C" || key === "\r") {
384
+ if (!this.state.loading && this.state.items.length > 0) {
385
+ if (navItems[this.state.navIndex]?.id === "settings")
386
+ this.state.mode = "settings";
387
+ this.state.focus = "content";
388
+ this.state.itemIndex = 0;
389
+ this.state.status = getStatus(this.state);
390
+ this.render();
391
+ }
392
+ return;
393
+ }
394
+ if (key === "r")
395
+ void this.load(true);
396
+ }
397
+ handleContentKey(key) {
398
+ if (key === "j" || key === "\x1b[B") {
399
+ this.state.itemIndex = Math.min(Math.max(0, this.state.items.length - 1), this.state.itemIndex + 1);
400
+ this.render();
401
+ return;
402
+ }
403
+ if (key === "k" || key === "\x1b[A") {
404
+ this.state.itemIndex = Math.max(0, this.state.itemIndex - 1);
405
+ this.render();
406
+ return;
407
+ }
408
+ if (key === "h" || key === "\x1b[D" || key === "\x1b") {
409
+ this.leave();
410
+ return;
411
+ }
412
+ if (key === "l" || key === "\x1b[C" || key === "\r") {
413
+ const selected = this.state.items[this.state.itemIndex];
414
+ if (selected) {
415
+ void this.activateContentItem(selected, this.nextSignal());
416
+ }
417
+ else {
418
+ this.state.status = "当前条目不可进入";
419
+ this.render();
420
+ }
421
+ return;
422
+ }
423
+ if ((key === "n" || key === " ") && this.state.currentChat) {
424
+ void this.loadNextChatPage(this.nextSignal());
425
+ return;
426
+ }
427
+ if (key === "r")
428
+ void this.refresh();
429
+ if (key === "/")
430
+ this.openSearch();
431
+ if (key === "o")
432
+ this.openMenu();
433
+ }
434
+ leave() {
435
+ this.abortCurrent();
436
+ if (this.state.mode === "topic") {
437
+ this.state.mode = "list";
438
+ this.state.focus = "content";
439
+ this.state.status = "";
440
+ this.render();
441
+ return;
442
+ }
443
+ if (this.state.parentList) {
444
+ this.restoreParentList();
445
+ this.render();
446
+ return;
447
+ }
448
+ this.state.focus = "nav";
449
+ this.state.status = "";
450
+ this.render();
451
+ }
452
+ refresh() {
453
+ if (this.state.currentBoard) {
454
+ void this.openBoard(this.state.currentBoard.boardId, this.state.currentBoard.title, true, this.nextSignal(), false);
455
+ }
456
+ else if (this.state.currentChat) {
457
+ void this.openChat(this.state.currentChat.userId, this.state.currentChat.title, true, this.nextSignal(), false);
458
+ }
459
+ else {
460
+ void this.load(true);
461
+ }
462
+ }
463
+ openSearch() {
464
+ this.state.modal = "search";
465
+ this.state.searchQuery = "";
466
+ this.state.searchResults = [];
467
+ this.state.searchMode = "topics";
468
+ this.state.searchScope = this.getSearchScope();
469
+ this.state.itemIndex = 0;
470
+ this.render();
471
+ }
472
+ getSearchScope() {
473
+ // 根据当前位置确定搜索范围
474
+ if (this.state.currentBoard) {
475
+ return { label: this.state.currentBoard.title, boardId: this.state.currentBoard.boardId };
476
+ }
477
+ return { label: "全站" };
478
+ }
479
+ openMenu() {
480
+ this.state.modal = "menu";
481
+ this.state.menuItems = this.getMenuItems();
482
+ this.state.menuIndex = 0;
483
+ this.render();
484
+ }
485
+ getMenuItems() {
486
+ if (this.state.mode === "topic") {
487
+ return [
488
+ { label: "刷新", key: "r", action: "refresh" },
489
+ { label: "返回列表", key: "h", action: "back" }
490
+ ];
491
+ }
492
+ if (this.state.mode === "list") {
493
+ const items = [{ label: "刷新", key: "r", action: "refresh" }];
494
+ if (this.state.currentBoard)
495
+ items.push({ label: "返回版面列表", key: "h", action: "back" });
496
+ return items;
497
+ }
498
+ return [];
499
+ }
500
+ closeModal() {
501
+ this.state.modal = null;
502
+ this.state.infoTitle = undefined;
503
+ this.state.infoLines = [];
504
+ this.state.userDetail = undefined;
505
+ this.render();
506
+ }
507
+ async loadView(view, force, signal) {
508
+ switch (view) {
509
+ case "hot": {
510
+ const [index, unread] = await Promise.all([
511
+ this.client.getForumIndex(force, signal),
512
+ this.client.getUnreadCount(force, signal)
513
+ ]);
514
+ const indexObject = asObject(index);
515
+ const unreadObject = asObject(unread);
516
+ const hotTopics = asArray(indexObject.hotTopic ?? indexObject.manualHotTopic);
517
+ return {
518
+ title: "十大",
519
+ items: hotTopics.map((topic) => topicItem(topic)),
520
+ stats: unreadStats(unreadObject),
521
+ overview: overviewStats(indexObject, unreadObject)
522
+ };
523
+ }
524
+ case "new": {
525
+ const topics = asArray(await this.client.getNewTopics(0, 12, force, signal));
526
+ return { title: "最新", items: topics.map((topic) => topicItem(topic)), stats: [{ title: "新帖流", detail: `${topics.length} 条` }] };
527
+ }
528
+ case "boards": {
529
+ const sections = asArray(await this.client.getAllBoards(force, signal));
530
+ const boards = flattenBoards(sections);
531
+ return {
532
+ title: "版面",
533
+ items: boards.slice(0, 24),
534
+ stats: [{ title: "分区", detail: `${sections.length}` }, { title: "版面", detail: `${boards.length}` }],
535
+ status: "版面:j/k 选择 Enter 进入版面 h 返回 r 刷新"
536
+ };
537
+ }
538
+ case "following": {
539
+ const topics = asArray(await this.client.getFolloweeTopics(0, 12, force, signal));
540
+ return {
541
+ title: "关注",
542
+ items: topics.map((topic) => topicItem(topic)),
543
+ stats: [{ title: "关注动态", detail: `${topics.length} 条` }, { title: "缓存", detail: "30s" }],
544
+ status: "关注:j/k 选择 Enter 打开帖子 h 返回 r 刷新"
545
+ };
546
+ }
547
+ case "favorite": {
548
+ const [meRaw, sectionsRaw, topicFavorites] = await Promise.all([
549
+ this.client.getMe(force, signal),
550
+ this.client.getAllBoards(false, signal),
551
+ this.client.getFavoriteTopics(0, 6, 1, 0, force, signal)
552
+ ]);
553
+ const customBoards = asArray(asObject(meRaw).customBoards).filter((id) => typeof id === "number");
554
+ const allBoards = flattenBoards(asArray(sectionsRaw));
555
+ const boardById = new Map(allBoards.filter((board) => board.boardId !== undefined).map((board) => [board.boardId, board]));
556
+ const topicGroups = await mapLimit(customBoards, 3, async (boardId) => {
557
+ const board = boardById.get(boardId);
558
+ const topics = asArray(await this.client.getBoardTopics(boardId, 0, 3, false, force, signal));
559
+ return topics.map((topic) => topicItem(topic, board));
560
+ });
561
+ const boardTopics = topicGroups.flat().sort((left, right) => (right.sortTime ?? 0) - (left.sortTime ?? 0)).slice(0, 12);
562
+ return {
563
+ title: "收藏",
564
+ items: [
565
+ { title: "收藏主题", meta: "topic/me/favorite", detail: "查看收藏夹主题列表", action: "favorite-topics" },
566
+ { title: "收藏更新", meta: "topic/me/favorite?order=1", detail: "查看收藏主题更新", action: "favorite-updates" },
567
+ { title: "收藏分组", meta: "me/favorite-topic-group", detail: "查看收藏夹分组", action: "favorite-groups" },
568
+ ...asArray(topicFavorites).slice(0, 6).map((topic) => topicItem(topic)),
569
+ ...boardTopics
570
+ ],
571
+ stats: [
572
+ { title: "收藏版面", detail: `${customBoards.length} 个` },
573
+ { title: "收藏主题", detail: `${asArray(topicFavorites).length} 条` },
574
+ { title: "版面主题", detail: `${boardTopics.length} 条` }
575
+ ],
576
+ status: "收藏:j/k 选择 Enter 打开 h 返回 r 刷新"
577
+ };
578
+ }
579
+ case "messages": {
580
+ const [unread, recent] = await Promise.all([
581
+ this.client.getUnreadCount(force, signal),
582
+ this.client.getRecentChats(0, 10, force, signal)
583
+ ]);
584
+ const chats = asArray(recent);
585
+ const names = await loadChatUserNames(this.client, chats, force, signal);
586
+ return {
587
+ title: "消息",
588
+ items: chats.length > 0 ? chats.map((chat) => chatItem(chat, names)) : [{ title: "暂无最近私信", meta: "recent-contact-users" }],
589
+ stats: unreadStats(asObject(unread)),
590
+ status: "消息:j/k 选择 Enter 打开会话 h 返回 r 刷新"
591
+ };
592
+ }
593
+ case "notices": {
594
+ const unread = asObject(await this.client.getUnreadCount(force, signal));
595
+ return {
596
+ title: "通知",
597
+ items: [
598
+ { title: "系统通知", meta: `${unread.systemCount ?? 0} 未读`, detail: "查看系统通知列表", action: "notices:system" },
599
+ { title: "@ 通知", meta: `${unread.atCount ?? 0} 未读`, detail: "查看提到我的通知", action: "notices:at" },
600
+ { title: "回复通知", meta: `${unread.replyCount ?? 0} 未读`, detail: "查看回复我的通知", action: "notices:reply" }
601
+ ],
602
+ stats: unreadStats(unread),
603
+ status: "通知:j/k 选择 Enter 打开列表 h 返回 r 刷新"
604
+ };
605
+ }
606
+ case "me": {
607
+ const [me, cacheStats] = await Promise.all([this.client.getMe(force, signal), this.client.getCacheStats()]);
608
+ const meObject = asObject(me);
609
+ return {
610
+ title: "我的",
611
+ items: [
612
+ { title: String(meObject.name ?? "当前账号"), meta: `#${meObject.id ?? "?"}`, detail: String(meObject.levelTitle ?? meObject.groupName ?? ""), userId: asNumber(meObject.id) },
613
+ { title: "我的最近主题", meta: "me/recent-topic", detail: "查看自己最近发布或回复的主题", action: "recent-topics" },
614
+ { title: "浏览历史", meta: "me/browsing-record", detail: "查看最近浏览过的主题", action: "browse-history" },
615
+ { title: "粉丝列表", meta: "me/follower", detail: "查看关注我的用户", action: "followers" },
616
+ { title: "关注列表", meta: "me/followee", detail: "查看我关注的用户", action: "followees" },
617
+ { title: "随机主题", meta: "topic/random-recent", detail: "随机读取一组最近主题", action: "random-topics" },
618
+ { title: "每日签到", meta: "me/signin", detail: "执行签到", action: "signin" }
619
+ ],
620
+ stats: [
621
+ { title: "登录状态", detail: "已登录" },
622
+ { title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` }
623
+ ],
624
+ status: "我的:j/k 选择 Enter 打开 h 返回 r 刷新"
625
+ };
626
+ }
627
+ case "settings": {
628
+ const cacheStats = await this.client.getCacheStats();
629
+ return {
630
+ title: "设置",
631
+ items: [...settingsItems],
632
+ stats: [{ title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` }, { title: "版本", detail: `v${appVersion}` }],
633
+ status: "设置:j/k 选择 Enter 执行 h 返回"
634
+ };
635
+ }
636
+ }
637
+ }
638
+ async activateSetting(selected) {
639
+ if (!selected)
640
+ return;
641
+ if (selected.meta === "help") {
642
+ this.state.modal = "help";
643
+ this.render();
644
+ return;
645
+ }
646
+ if (selected.meta === "cache") {
647
+ this.state.status = "正在清理缓存...";
648
+ this.render();
649
+ try {
650
+ await this.client.clearCache();
651
+ this.state.status = "缓存已清理";
652
+ await this.load(true);
653
+ }
654
+ catch {
655
+ this.state.status = "缓存清理失败";
656
+ this.render();
657
+ }
658
+ return;
659
+ }
660
+ if (selected.meta === "update") {
661
+ this.state.status = "正在检查 GitHub Release...";
662
+ this.render();
663
+ try {
664
+ const result = await checkForUpdate();
665
+ this.state.status = result.message;
666
+ }
667
+ catch (error) {
668
+ this.state.status = error instanceof Error ? error.message : "检查更新失败";
669
+ }
670
+ this.render();
671
+ return;
672
+ }
673
+ this.state.status = selected.meta === "logout" ? "退出登录功能开发中..." : "账号切换功能开发中...";
674
+ this.render();
675
+ }
676
+ async activateContentItem(selected, signal) {
677
+ if (selected.topicId !== undefined) {
678
+ await this.openTopic(selected.topicId, false, signal);
679
+ return;
680
+ }
681
+ if (selected.boardId !== undefined) {
682
+ await this.openBoard(selected.boardId, selected.title, false, signal);
683
+ return;
684
+ }
685
+ if (selected.chatUserId !== undefined) {
686
+ await this.openChat(selected.chatUserId, selected.title, false, signal);
687
+ return;
688
+ }
689
+ if (selected.userId !== undefined) {
690
+ await this.showUserDetailById(selected.userId, signal);
691
+ return;
692
+ }
693
+ if (selected.action?.startsWith("notices:")) {
694
+ await this.openNoticeList(selected.action.split(":")[1], signal);
695
+ return;
696
+ }
697
+ if (selected.action) {
698
+ await this.runReadOnlyAction(selected.action, signal);
699
+ return;
700
+ }
701
+ this.state.status = "当前条目不可进入";
702
+ this.render();
703
+ }
704
+ async openTopic(topicId, force, signal) {
705
+ this.state.mode = "topic";
706
+ this.state.loading = true;
707
+ this.state.error = undefined;
708
+ this.state.status = "";
709
+ this.state.topic = undefined;
710
+ this.state.scroll = 0;
711
+ this.render();
712
+ try {
713
+ const [topicRaw, postsRaw] = await Promise.all([
714
+ this.client.getTopic(topicId, force, signal),
715
+ this.client.getTopicPosts(topicId, 0, 10, force, signal)
716
+ ]);
717
+ this.state.topic = buildTopicReader(topicId, asObject(topicRaw), asArray(postsRaw), 10);
718
+ this.state.loading = false;
719
+ this.state.status = "";
720
+ }
721
+ catch (error) {
722
+ if (!isAbortError(error)) {
723
+ this.state.error = error instanceof Error ? error.message : String(error);
724
+ this.state.loading = false;
725
+ }
726
+ }
727
+ this.render();
728
+ }
729
+ async openBoard(boardId, title, force, signal, pushParent = true) {
730
+ if (pushParent)
731
+ this.snapshotParent();
732
+ this.state.loading = true;
733
+ this.state.error = undefined;
734
+ this.state.viewTitle = title;
735
+ this.state.focus = "content";
736
+ this.state.currentBoard = { boardId, title };
737
+ this.state.itemIndex = 0;
738
+ this.state.scroll = 0;
739
+ this.render();
740
+ try {
741
+ const topics = asArray(await this.client.getBoardTopics(boardId, 0, 20, false, force, signal));
742
+ this.state.items = topics.map((topic) => topicItem(topic, { title, boardId }));
743
+ this.state.stats = [{ title: "主题", detail: `${topics.length} 条` }];
744
+ this.state.status = `版面 ${title}: j/k 选择 Enter 打开帖子 h 返回`;
745
+ }
746
+ catch (error) {
747
+ if (!isAbortError(error))
748
+ this.state.error = error instanceof Error ? error.message : String(error);
749
+ }
750
+ finally {
751
+ this.state.loading = false;
752
+ this.render();
753
+ }
754
+ }
755
+ async openChat(userId, title, force, signal, pushParent = true) {
756
+ if (pushParent)
757
+ this.snapshotParent();
758
+ this.state.loading = true;
759
+ this.state.error = undefined;
760
+ this.state.viewTitle = `私信: ${title}`;
761
+ this.state.focus = "content";
762
+ this.state.itemIndex = 0;
763
+ this.state.scroll = 0;
764
+ this.render();
765
+ try {
766
+ const messages = asArray(await this.client.getChatHistory(userId, 0, 10, force, signal));
767
+ this.state.items = chatMessageItems(messages, title, userId);
768
+ this.state.currentChat = { userId, title, loaded: messages.length, size: 10, hasMore: messages.length >= 10 };
769
+ this.state.stats = [{ title: "会话", detail: title }, { title: "消息", detail: `${messages.length}` }];
770
+ this.state.status = "私信:n 加载更多 h 返回";
771
+ }
772
+ catch (error) {
773
+ if (!isAbortError(error))
774
+ this.state.error = error instanceof Error ? error.message : String(error);
775
+ }
776
+ finally {
777
+ this.state.loading = false;
778
+ this.render();
779
+ }
780
+ }
781
+ async loadNextChatPage(signal) {
782
+ const chat = this.state.currentChat;
783
+ if (!chat?.hasMore || this.state.loadingMore)
784
+ return;
785
+ this.state.loadingMore = true;
786
+ this.render();
787
+ try {
788
+ const messages = asArray(await this.client.getChatHistory(chat.userId, chat.loaded, chat.size, false, signal));
789
+ this.state.items.push(...chatMessageItems(messages, chat.title, chat.userId));
790
+ chat.loaded += messages.length;
791
+ chat.hasMore = messages.length >= chat.size;
792
+ this.state.status = chat.hasMore ? "已加载更多私信" : "已到底";
793
+ }
794
+ catch (error) {
795
+ if (!isAbortError(error))
796
+ this.state.status = error instanceof Error ? error.message : "加载失败";
797
+ }
798
+ finally {
799
+ this.state.loadingMore = false;
800
+ this.render();
801
+ }
802
+ }
803
+ async loadNextTopicPage(signal, quiet = false) {
804
+ const topic = this.state.topic;
805
+ if (!topic?.hasMore || this.state.loadingMore)
806
+ return;
807
+ this.state.loadingMore = true;
808
+ if (!quiet)
809
+ this.render();
810
+ try {
811
+ const posts = asArray(await this.client.getTopicPosts(topic.topicId, topic.loaded, topic.size, false, signal));
812
+ appendTopicPosts(topic, posts);
813
+ this.state.status = topic.hasMore ? "已加载下一页" : "已到底";
814
+ }
815
+ catch (error) {
816
+ if (!isAbortError(error))
817
+ this.state.status = error instanceof Error ? error.message : "加载失败";
818
+ }
819
+ finally {
820
+ this.state.loadingMore = false;
821
+ this.render();
822
+ }
823
+ }
824
+ async jumpToTopicFloor(floor, signal) {
825
+ const topic = this.state.topic;
826
+ if (!topic)
827
+ return;
828
+ const loaded = findTopicPostByFloor(topic, floor);
829
+ if (loaded) {
830
+ this.state.scroll = loaded.lineStart;
831
+ this.state.status = getStatus(this.state);
832
+ this.render();
833
+ return;
834
+ }
835
+ while (topic.hasMore && !findTopicPostByFloor(topic, floor)) {
836
+ await this.loadNextTopicPage(signal, true);
837
+ }
838
+ const post = findTopicPostByFloor(topic, floor);
839
+ this.state.scroll = post?.lineStart ?? this.state.scroll;
840
+ this.state.status = post ? getStatus(this.state) : `未找到 ${floor} 楼`;
841
+ this.render();
842
+ }
843
+ async runReadOnlyAction(action, signal) {
844
+ switch (action) {
845
+ case "random-topics": {
846
+ const topics = asArray(await this.client.getRandomTopics(10, true, signal));
847
+ this.openReadOnlyList("随机主题", topics.map((topic) => topicItem(topic)), [{ title: "主题", detail: `${topics.length}` }]);
848
+ return;
849
+ }
850
+ case "recent-topics": {
851
+ const topics = asArray(await this.client.getRecentTopics(undefined, 0, 11, false, signal));
852
+ this.openReadOnlyList("我的最近主题", topics.map((topic) => topicItem(topic)), [{ title: "主题", detail: `${topics.length}` }]);
853
+ return;
854
+ }
855
+ case "browse-history": {
856
+ const topics = asArray(await this.client.getBrowseHistory(0, 11, false, signal));
857
+ this.openReadOnlyList("浏览历史", topics.map((topic) => historyItem(topic)), [{ title: "记录", detail: `${topics.length}` }]);
858
+ return;
859
+ }
860
+ case "favorite-topics":
861
+ case "favorite-updates": {
862
+ const order = action === "favorite-updates" ? 1 : 0;
863
+ const topics = asArray(await this.client.getFavoriteTopics(0, 11, order, 0, false, signal));
864
+ this.openReadOnlyList(action === "favorite-updates" ? "收藏更新" : "收藏主题", topics.map((topic) => topicItem(topic)), [{ title: "主题", detail: `${topics.length}` }]);
865
+ return;
866
+ }
867
+ case "favorite-groups": {
868
+ const groups = asArray(await this.client.getFavoriteGroups(false, signal));
869
+ this.openReadOnlyList("收藏分组", groups.map((group) => genericItem(group, "收藏分组")), [{ title: "分组", detail: `${groups.length}` }]);
870
+ return;
871
+ }
872
+ case "followers":
873
+ case "followees": {
874
+ await this.openFriendUsers(action === "followers" ? "follower" : "followee", signal);
875
+ return;
876
+ }
877
+ case "card-stat": {
878
+ const stat = await this.client.getCardStat(false, signal);
879
+ this.state.modal = "info";
880
+ this.state.infoTitle = "全站统计";
881
+ this.state.infoLines = jsonPreviewLines(stat);
882
+ this.render();
883
+ return;
884
+ }
885
+ case "rate-reasons:0":
886
+ case "rate-reasons:1": {
887
+ const type = action.endsWith(":1") ? 1 : 0;
888
+ const reasons = asArray(await this.client.getPostRateReasons(type, false, signal));
889
+ this.openReadOnlyList(type === 1 ? "评分原因: 管理" : "评分原因: 普通", reasons.map((reason) => genericItem(reason, "评分原因")), [{ title: "原因", detail: `${reasons.length}` }]);
890
+ return;
891
+ }
892
+ case "signin": {
893
+ await this.signin();
894
+ return;
895
+ }
896
+ }
897
+ this.state.status = "暂不支持该入口";
898
+ this.render();
899
+ }
900
+ async openNoticeList(type, signal) {
901
+ const notices = asArray(await this.client.getNotices(type, 0, 10, false, signal));
902
+ const titleMap = { system: "系统通知", at: "@ 通知", reply: "回复通知" };
903
+ this.openReadOnlyList(titleMap[type], notices.map((notice) => noticeItem(notice)), [{ title: "通知", detail: `${notices.length}` }]);
904
+ }
905
+ async performSearch(signal) {
906
+ const query = this.state.searchQuery.trim();
907
+ if (!query)
908
+ return;
909
+ this.state.loading = true;
910
+ this.render();
911
+ try {
912
+ const results = this.state.searchMode === "topics"
913
+ ? this.filterSearchTopicScope(asArray(await this.client.searchTopics(query, 0, 20, true, signal)).map((topic) => topicItem(topic)))
914
+ : asArray(await this.client.searchUsers(query, true, signal)).map((user) => userItem(user));
915
+ this.state.searchResults = results;
916
+ this.state.itemIndex = 0;
917
+ }
918
+ catch (error) {
919
+ if (!isAbortError(error))
920
+ this.state.status = error instanceof Error ? error.message : "搜索失败";
921
+ }
922
+ finally {
923
+ this.state.loading = false;
924
+ this.render();
925
+ }
926
+ }
927
+ filterSearchTopicScope(items) {
928
+ const boardId = this.state.searchScope.boardId;
929
+ if (boardId === undefined) {
930
+ return items;
931
+ }
932
+ return items.filter((item) => item.boardId === boardId);
933
+ }
934
+ async toggleFavorite() {
935
+ const topic = this.state.topic;
936
+ if (!topic)
937
+ return;
938
+ try {
939
+ const isFavorite = await this.client.isTopicFavorite(topic.topicId, true);
940
+ if (isFavorite) {
941
+ await this.client.removeFavorite(topic.topicId);
942
+ this.state.status = "已取消收藏";
943
+ }
944
+ else {
945
+ await this.client.addFavorite(topic.topicId);
946
+ this.state.status = "已收藏";
947
+ }
948
+ }
949
+ catch (error) {
950
+ this.state.status = error instanceof Error ? error.message : "收藏操作失败";
951
+ }
952
+ this.render();
953
+ }
954
+ async reactToCurrentPost(isLike) {
955
+ const topic = this.state.topic;
956
+ if (!topic)
957
+ return;
958
+ const post = currentTopicPost(topic, this.state.scroll);
959
+ if (!post?.id) {
960
+ this.state.status = "当前楼层没有可操作的帖子 ID";
961
+ this.render();
962
+ return;
963
+ }
964
+ try {
965
+ await this.client.reactToPost(post.id, isLike);
966
+ this.state.status = isLike ? "已点赞" : "已踩";
967
+ }
968
+ catch (error) {
969
+ this.state.status = error instanceof Error ? error.message : "操作失败";
970
+ }
971
+ this.render();
972
+ }
973
+ async showCurrentUser(signal) {
974
+ const topic = this.state.topic;
975
+ if (!topic)
976
+ return;
977
+ const post = currentTopicPost(topic, this.state.scroll);
978
+ if (!post?.userId) {
979
+ this.state.status = "当前楼层没有用户 ID";
980
+ this.render();
981
+ return;
982
+ }
983
+ await this.showUserDetailById(post.userId, signal);
984
+ }
985
+ async showUserDetailById(userId, signal) {
986
+ this.state.status = "正在读取用户信息...";
987
+ this.render();
988
+ try {
989
+ const [profileRaw, recentRaw] = await Promise.all([
990
+ this.client.getUserProfile(userId, false, signal),
991
+ this.client.getRecentTopics(userId, 0, 5, false, signal)
992
+ ]);
993
+ const profile = asObject(profileRaw);
994
+ this.state.userDetail = {
995
+ userId,
996
+ name: String(profile.name ?? `#${userId}`),
997
+ level: String(profile.levelTitle ?? profile.groupName ?? ""),
998
+ postCount: asNumber(profile.postCount),
999
+ fanCount: asNumber(profile.fanCount),
1000
+ followCount: asNumber(profile.followCount),
1001
+ isFollowing: Boolean(profile.isFollowing),
1002
+ recentTopics: asArray(recentRaw).map((topic) => topicItem(topic))
1003
+ };
1004
+ this.state.modal = "user";
1005
+ this.state.status = getStatus(this.state);
1006
+ }
1007
+ catch (error) {
1008
+ if (!isAbortError(error))
1009
+ this.state.status = error instanceof Error ? error.message : "读取用户失败";
1010
+ }
1011
+ this.render();
1012
+ }
1013
+ async showTopicVote(signal) {
1014
+ const topic = this.state.topic;
1015
+ if (!topic)
1016
+ return;
1017
+ try {
1018
+ const vote = await this.client.getTopicVote(topic.topicId, false, signal);
1019
+ this.state.modal = "info";
1020
+ this.state.infoTitle = "投票信息";
1021
+ this.state.infoLines = jsonPreviewLines(vote);
1022
+ }
1023
+ catch (error) {
1024
+ if (!isAbortError(error))
1025
+ this.state.status = error instanceof Error ? error.message : "读取投票失败";
1026
+ }
1027
+ this.render();
1028
+ }
1029
+ async showPostReactionState(signal) {
1030
+ const topic = this.state.topic;
1031
+ if (!topic)
1032
+ return;
1033
+ const post = currentTopicPost(topic, this.state.scroll);
1034
+ if (!post?.id)
1035
+ return;
1036
+ try {
1037
+ const state = await this.client.getPostReactionState(post.id, true, signal);
1038
+ this.state.modal = "info";
1039
+ this.state.infoTitle = "楼层评价";
1040
+ this.state.infoLines = jsonPreviewLines(state);
1041
+ }
1042
+ catch (error) {
1043
+ if (!isAbortError(error))
1044
+ this.state.status = error instanceof Error ? error.message : "读取评价失败";
1045
+ }
1046
+ this.render();
1047
+ }
1048
+ async toggleFollow() {
1049
+ const user = this.state.userDetail;
1050
+ if (!user)
1051
+ return;
1052
+ try {
1053
+ if (user.isFollowing) {
1054
+ await this.client.unfollowUser(user.userId);
1055
+ user.isFollowing = false;
1056
+ this.state.status = "已取消关注";
1057
+ }
1058
+ else {
1059
+ await this.client.followUser(user.userId);
1060
+ user.isFollowing = true;
1061
+ this.state.status = "已关注";
1062
+ }
1063
+ }
1064
+ catch (error) {
1065
+ this.state.status = error instanceof Error ? error.message : "关注操作失败";
1066
+ }
1067
+ this.render();
1068
+ }
1069
+ async sendPrivateMessage(userId, content) {
1070
+ try {
1071
+ await this.client.sendMessage(userId, content);
1072
+ this.state.status = "私信已发送";
1073
+ }
1074
+ catch (error) {
1075
+ this.state.status = error instanceof Error ? error.message : "发送失败";
1076
+ }
1077
+ this.render();
1078
+ }
1079
+ getCurrentTopicLine() {
1080
+ const topic = this.state.topic;
1081
+ if (!topic)
1082
+ return undefined;
1083
+ for (const post of topic.posts) {
1084
+ const line = post.lines.find((entry) => entry.line === this.state.scroll);
1085
+ if (line)
1086
+ return line;
1087
+ }
1088
+ return undefined;
1089
+ }
1090
+ async openImage(url) {
1091
+ this.state.status = "正在下载图片...";
1092
+ this.render();
1093
+ try {
1094
+ const cache = getImageCache();
1095
+ const localPath = await cache.getOrDownload(url);
1096
+ this.state.status = `已缓存: ${localPath}`;
1097
+ this.render();
1098
+ // 用系统默认程序打开图片
1099
+ const { execFile } = await import("node:child_process");
1100
+ const platform = process.platform;
1101
+ const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
1102
+ const args = platform === "win32" ? ["/c", "start", "", localPath] : [localPath];
1103
+ execFile(command, args, (error) => {
1104
+ if (error) {
1105
+ this.state.status = `打开失败: ${error.message}`;
1106
+ this.render();
1107
+ }
1108
+ });
1109
+ }
1110
+ catch (error) {
1111
+ this.state.status = error instanceof Error ? error.message : "图片下载失败";
1112
+ this.render();
1113
+ }
1114
+ }
1115
+ async copyToClipboard(text) {
1116
+ try {
1117
+ const { spawn } = await import("node:child_process");
1118
+ const platform = process.platform;
1119
+ const command = platform === "win32" ? "clip" : platform === "darwin" ? "pbcopy" : "xclip";
1120
+ const args = platform === "linux" ? ["-selection", "clipboard"] : [];
1121
+ const child = spawn(command, args);
1122
+ child.stdin.end(text);
1123
+ child.on("error", () => {
1124
+ this.state.status = "复制失败";
1125
+ this.render();
1126
+ });
1127
+ child.on("close", (code) => {
1128
+ this.state.status = code === 0 ? "已复制到剪贴板" : "复制失败";
1129
+ this.render();
1130
+ });
1131
+ }
1132
+ catch {
1133
+ this.state.status = "复制失败";
1134
+ this.render();
1135
+ }
1136
+ }
1137
+ async signin() {
1138
+ this.state.status = "正在签到...";
1139
+ this.render();
1140
+ try {
1141
+ const result = await this.client.signin();
1142
+ this.state.modal = "info";
1143
+ this.state.infoTitle = "签到结果";
1144
+ this.state.infoLines = jsonPreviewLines(result);
1145
+ }
1146
+ catch (error) {
1147
+ this.state.status = error instanceof Error ? error.message : "签到失败";
1148
+ }
1149
+ this.render();
1150
+ }
1151
+ async openFriendUsers(type, signal) {
1152
+ const ids = asArray(await this.client.getFriendIds(type, 0, 20, false, signal)).filter((id) => typeof id === "number");
1153
+ const users = asArray(await this.client.getUsers(ids, false, signal));
1154
+ this.openReadOnlyList(type === "follower" ? "粉丝列表" : "关注列表", users.map((user) => userItem(user)), [{ title: "用户", detail: `${users.length}` }]);
1155
+ }
1156
+ openReadOnlyList(title, items, stats) {
1157
+ this.snapshotParent();
1158
+ this.state.viewTitle = title;
1159
+ this.state.items = items;
1160
+ this.state.stats = stats;
1161
+ this.state.itemIndex = 0;
1162
+ this.state.scroll = 0;
1163
+ this.state.focus = "content";
1164
+ this.state.currentBoard = undefined;
1165
+ this.state.currentChat = undefined;
1166
+ this.state.topic = undefined;
1167
+ this.state.mode = "list";
1168
+ this.state.status = `${title}: j/k 选择 Enter 打开 h 返回`;
1169
+ this.render();
1170
+ }
1171
+ snapshotParent() {
1172
+ if (!this.state.parentList) {
1173
+ this.state.parentList = {
1174
+ title: this.state.viewTitle,
1175
+ items: [...this.state.items],
1176
+ stats: [...this.state.stats],
1177
+ itemIndex: this.state.itemIndex,
1178
+ status: this.state.status
1179
+ };
1180
+ }
1181
+ }
1182
+ restoreParentList() {
1183
+ const parent = this.state.parentList;
1184
+ if (!parent)
1185
+ return;
1186
+ this.state.viewTitle = parent.title;
1187
+ this.state.items = parent.items;
1188
+ this.state.stats = parent.stats;
1189
+ this.state.itemIndex = parent.itemIndex;
1190
+ this.state.status = parent.status;
1191
+ this.state.parentList = undefined;
1192
+ this.state.currentBoard = undefined;
1193
+ this.state.currentChat = undefined;
1194
+ this.state.topic = undefined;
1195
+ this.state.mode = "list";
1196
+ this.state.focus = "content";
1197
+ this.state.scroll = 0;
1198
+ }
1199
+ }
1200
+ //# sourceMappingURL=controller.js.map