@walavave/cc98-cli 0.1.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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/api/client.d.ts +57 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +298 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/endpoints.d.ts +48 -0
  8. package/dist/api/endpoints.d.ts.map +1 -0
  9. package/dist/api/endpoints.js +53 -0
  10. package/dist/api/endpoints.js.map +1 -0
  11. package/dist/api/types.d.ts +35 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/api/types.js +2 -0
  14. package/dist/api/types.js.map +1 -0
  15. package/dist/api/webvpn.d.ts +40 -0
  16. package/dist/api/webvpn.d.ts.map +1 -0
  17. package/dist/api/webvpn.js +259 -0
  18. package/dist/api/webvpn.js.map +1 -0
  19. package/dist/cli/commands/account.d.ts +2 -0
  20. package/dist/cli/commands/account.d.ts.map +1 -0
  21. package/dist/cli/commands/account.js +81 -0
  22. package/dist/cli/commands/account.js.map +1 -0
  23. package/dist/cli/commands/board.d.ts +2 -0
  24. package/dist/cli/commands/board.d.ts.map +1 -0
  25. package/dist/cli/commands/board.js +61 -0
  26. package/dist/cli/commands/board.js.map +1 -0
  27. package/dist/cli/commands/cache.d.ts +2 -0
  28. package/dist/cli/commands/cache.d.ts.map +1 -0
  29. package/dist/cli/commands/cache.js +68 -0
  30. package/dist/cli/commands/cache.js.map +1 -0
  31. package/dist/cli/commands/forum.d.ts +2 -0
  32. package/dist/cli/commands/forum.d.ts.map +1 -0
  33. package/dist/cli/commands/forum.js +38 -0
  34. package/dist/cli/commands/forum.js.map +1 -0
  35. package/dist/cli/commands/login.d.ts +2 -0
  36. package/dist/cli/commands/login.d.ts.map +1 -0
  37. package/dist/cli/commands/login.js +98 -0
  38. package/dist/cli/commands/login.js.map +1 -0
  39. package/dist/cli/commands/logout.d.ts +2 -0
  40. package/dist/cli/commands/logout.d.ts.map +1 -0
  41. package/dist/cli/commands/logout.js +20 -0
  42. package/dist/cli/commands/logout.js.map +1 -0
  43. package/dist/cli/commands/me.d.ts +2 -0
  44. package/dist/cli/commands/me.d.ts.map +1 -0
  45. package/dist/cli/commands/me.js +9 -0
  46. package/dist/cli/commands/me.js.map +1 -0
  47. package/dist/cli/commands/message.d.ts +2 -0
  48. package/dist/cli/commands/message.d.ts.map +1 -0
  49. package/dist/cli/commands/message.js +42 -0
  50. package/dist/cli/commands/message.js.map +1 -0
  51. package/dist/cli/commands/notice.d.ts +2 -0
  52. package/dist/cli/commands/notice.d.ts.map +1 -0
  53. package/dist/cli/commands/notice.js +30 -0
  54. package/dist/cli/commands/notice.js.map +1 -0
  55. package/dist/cli/commands/post.d.ts +2 -0
  56. package/dist/cli/commands/post.d.ts.map +1 -0
  57. package/dist/cli/commands/post.js +37 -0
  58. package/dist/cli/commands/post.js.map +1 -0
  59. package/dist/cli/commands/search.d.ts +2 -0
  60. package/dist/cli/commands/search.d.ts.map +1 -0
  61. package/dist/cli/commands/search.js +13 -0
  62. package/dist/cli/commands/search.js.map +1 -0
  63. package/dist/cli/commands/topic.d.ts +2 -0
  64. package/dist/cli/commands/topic.d.ts.map +1 -0
  65. package/dist/cli/commands/topic.js +196 -0
  66. package/dist/cli/commands/topic.js.map +1 -0
  67. package/dist/cli/commands/update.d.ts +2 -0
  68. package/dist/cli/commands/update.d.ts.map +1 -0
  69. package/dist/cli/commands/update.js +24 -0
  70. package/dist/cli/commands/update.js.map +1 -0
  71. package/dist/cli/commands/user.d.ts +2 -0
  72. package/dist/cli/commands/user.d.ts.map +1 -0
  73. package/dist/cli/commands/user.js +90 -0
  74. package/dist/cli/commands/user.js.map +1 -0
  75. package/dist/cli/commands/vpn.d.ts +2 -0
  76. package/dist/cli/commands/vpn.d.ts.map +1 -0
  77. package/dist/cli/commands/vpn.js +177 -0
  78. package/dist/cli/commands/vpn.js.map +1 -0
  79. package/dist/cli/context.d.ts +9 -0
  80. package/dist/cli/context.d.ts.map +1 -0
  81. package/dist/cli/context.js +26 -0
  82. package/dist/cli/context.js.map +1 -0
  83. package/dist/cli/options.d.ts +6 -0
  84. package/dist/cli/options.d.ts.map +1 -0
  85. package/dist/cli/options.js +22 -0
  86. package/dist/cli/options.js.map +1 -0
  87. package/dist/cli/parse.d.ts +15 -0
  88. package/dist/cli/parse.d.ts.map +1 -0
  89. package/dist/cli/parse.js +55 -0
  90. package/dist/cli/parse.js.map +1 -0
  91. package/dist/cli/prompt.d.ts +4 -0
  92. package/dist/cli/prompt.d.ts.map +1 -0
  93. package/dist/cli/prompt.js +53 -0
  94. package/dist/cli/prompt.js.map +1 -0
  95. package/dist/cli/router.d.ts +2 -0
  96. package/dist/cli/router.d.ts.map +1 -0
  97. package/dist/cli/router.js +90 -0
  98. package/dist/cli/router.js.map +1 -0
  99. package/dist/config.d.ts +10 -0
  100. package/dist/config.d.ts.map +1 -0
  101. package/dist/config.js +74 -0
  102. package/dist/config.js.map +1 -0
  103. package/dist/main.d.ts +3 -0
  104. package/dist/main.d.ts.map +1 -0
  105. package/dist/main.js +17 -0
  106. package/dist/main.js.map +1 -0
  107. package/dist/storage/cache-store.d.ts +62 -0
  108. package/dist/storage/cache-store.d.ts.map +1 -0
  109. package/dist/storage/cache-store.js +233 -0
  110. package/dist/storage/cache-store.js.map +1 -0
  111. package/dist/storage/paths.d.ts +5 -0
  112. package/dist/storage/paths.d.ts.map +1 -0
  113. package/dist/storage/paths.js +17 -0
  114. package/dist/storage/paths.js.map +1 -0
  115. package/dist/storage/token-store.d.ts +39 -0
  116. package/dist/storage/token-store.d.ts.map +1 -0
  117. package/dist/storage/token-store.js +177 -0
  118. package/dist/storage/token-store.js.map +1 -0
  119. package/dist/storage/vpn-store.d.ts +16 -0
  120. package/dist/storage/vpn-store.d.ts.map +1 -0
  121. package/dist/storage/vpn-store.js +72 -0
  122. package/dist/storage/vpn-store.js.map +1 -0
  123. package/dist/tui/account-modal.d.ts +31 -0
  124. package/dist/tui/account-modal.d.ts.map +1 -0
  125. package/dist/tui/account-modal.js +97 -0
  126. package/dist/tui/account-modal.js.map +1 -0
  127. package/dist/tui/ansi.d.ts +20 -0
  128. package/dist/tui/ansi.d.ts.map +1 -0
  129. package/dist/tui/ansi.js +28 -0
  130. package/dist/tui/ansi.js.map +1 -0
  131. package/dist/tui/app-data.d.ts +22 -0
  132. package/dist/tui/app-data.d.ts.map +1 -0
  133. package/dist/tui/app-data.js +828 -0
  134. package/dist/tui/app-data.js.map +1 -0
  135. package/dist/tui/app-runtime.d.ts +27 -0
  136. package/dist/tui/app-runtime.d.ts.map +1 -0
  137. package/dist/tui/app-runtime.js +874 -0
  138. package/dist/tui/app-runtime.js.map +1 -0
  139. package/dist/tui/app.d.ts +2 -0
  140. package/dist/tui/app.d.ts.map +1 -0
  141. package/dist/tui/app.js +172 -0
  142. package/dist/tui/app.js.map +1 -0
  143. package/dist/tui/cached-client.d.ts +41 -0
  144. package/dist/tui/cached-client.d.ts.map +1 -0
  145. package/dist/tui/cached-client.js +78 -0
  146. package/dist/tui/cached-client.js.map +1 -0
  147. package/dist/tui/canvas.d.ts +26 -0
  148. package/dist/tui/canvas.d.ts.map +1 -0
  149. package/dist/tui/canvas.js +204 -0
  150. package/dist/tui/canvas.js.map +1 -0
  151. package/dist/tui/downloads.d.ts +2 -0
  152. package/dist/tui/downloads.d.ts.map +1 -0
  153. package/dist/tui/downloads.js +81 -0
  154. package/dist/tui/downloads.js.map +1 -0
  155. package/dist/tui/image-preview.d.ts +19 -0
  156. package/dist/tui/image-preview.d.ts.map +1 -0
  157. package/dist/tui/image-preview.js +217 -0
  158. package/dist/tui/image-preview.js.map +1 -0
  159. package/dist/tui/interactions.d.ts +5 -0
  160. package/dist/tui/interactions.d.ts.map +1 -0
  161. package/dist/tui/interactions.js +25 -0
  162. package/dist/tui/interactions.js.map +1 -0
  163. package/dist/tui/keymap.d.ts +7 -0
  164. package/dist/tui/keymap.d.ts.map +1 -0
  165. package/dist/tui/keymap.js +262 -0
  166. package/dist/tui/keymap.js.map +1 -0
  167. package/dist/tui/layout.d.ts +29 -0
  168. package/dist/tui/layout.d.ts.map +1 -0
  169. package/dist/tui/layout.js +82 -0
  170. package/dist/tui/layout.js.map +1 -0
  171. package/dist/tui/renderer.d.ts +9 -0
  172. package/dist/tui/renderer.d.ts.map +1 -0
  173. package/dist/tui/renderer.js +375 -0
  174. package/dist/tui/renderer.js.map +1 -0
  175. package/dist/tui/terminal.d.ts +41 -0
  176. package/dist/tui/terminal.d.ts.map +1 -0
  177. package/dist/tui/terminal.js +162 -0
  178. package/dist/tui/terminal.js.map +1 -0
  179. package/dist/tui/text.d.ts +10 -0
  180. package/dist/tui/text.d.ts.map +1 -0
  181. package/dist/tui/text.js +158 -0
  182. package/dist/tui/text.js.map +1 -0
  183. package/dist/tui/theme.d.ts +56 -0
  184. package/dist/tui/theme.d.ts.map +1 -0
  185. package/dist/tui/theme.js +95 -0
  186. package/dist/tui/theme.js.map +1 -0
  187. package/dist/tui/tui-model.d.ts +126 -0
  188. package/dist/tui/tui-model.d.ts.map +1 -0
  189. package/dist/tui/tui-model.js +54 -0
  190. package/dist/tui/tui-model.js.map +1 -0
  191. package/dist/tui/ubb-renderer.d.ts +12 -0
  192. package/dist/tui/ubb-renderer.d.ts.map +1 -0
  193. package/dist/tui/ubb-renderer.js +196 -0
  194. package/dist/tui/ubb-renderer.js.map +1 -0
  195. package/dist/update.d.ts +17 -0
  196. package/dist/update.d.ts.map +1 -0
  197. package/dist/update.js +88 -0
  198. package/dist/update.js.map +1 -0
  199. package/dist/version.d.ts +6 -0
  200. package/dist/version.d.ts.map +1 -0
  201. package/dist/version.js +6 -0
  202. package/dist/version.js.map +1 -0
  203. package/docs/images/tui.png +0 -0
  204. package/docs/images/tui2.png +0 -0
  205. package/package.json +43 -0
@@ -0,0 +1,828 @@
1
+ import { imagePreviewRows, loadImagePreview, measureImagePreview, supportsImagePreview } from "./image-preview.js";
2
+ import { getSidebarWidth } from "./renderer.js";
3
+ import { theme } from "./theme.js";
4
+ import { currentTopicPost, getStatus, settingsItems } from "./tui-model.js";
5
+ import { renderMarkdownToLines, renderUbbToLines } from "./ubb-renderer.js";
6
+ export async function openTopic(client, state, topicId, render, config, force = false, signal) {
7
+ state.mode = "topic";
8
+ state.loading = true;
9
+ state.loadingMore = false;
10
+ state.error = undefined;
11
+ state.scroll = 0;
12
+ state.imageViewer = undefined;
13
+ state.topic = {
14
+ topicId,
15
+ title: `#${topicId}`,
16
+ meta: "",
17
+ lines: [],
18
+ posts: [],
19
+ loaded: 0,
20
+ size: 10,
21
+ hasMore: true,
22
+ imageCount: 0,
23
+ linkCount: 0,
24
+ floorInput: ""
25
+ };
26
+ state.status = "正在打开帖子...";
27
+ render();
28
+ try {
29
+ const [topicRaw, postsRaw] = await Promise.all([
30
+ client.getTopic(topicId, force, signal),
31
+ client.getTopicPosts(topicId, 0, 10, force, signal)
32
+ ]);
33
+ const topic = asObject(topicRaw);
34
+ const posts = asArray(postsRaw);
35
+ const reader = buildTopicReader(topicId, topic, posts, 10, config);
36
+ state.topic = reader;
37
+ state.viewTitle = reader.title;
38
+ void loadTopicImagePreviews(state, render, config, state.sidebarWidth);
39
+ }
40
+ catch (error) {
41
+ if (isAbortError(error)) {
42
+ return;
43
+ }
44
+ state.error = error instanceof Error ? error.message : String(error);
45
+ state.status = state.parentList
46
+ ? "版面读取失败;Esc/Backspace 返回版面列表 h 返回左栏 r 重试"
47
+ : "版面读取失败;h 返回左栏 r 重试";
48
+ }
49
+ finally {
50
+ state.loading = false;
51
+ if (!state.error && state.mode === "topic" && state.topic) {
52
+ state.status = getStatus(state);
53
+ }
54
+ render();
55
+ }
56
+ }
57
+ export async function openBoard(client, state, boardId, boardTitle, render, force = false, signal, pushParent = true) {
58
+ prepareListView(state, {
59
+ title: boardTitle,
60
+ status: "正在读取版面帖子...",
61
+ currentBoard: { boardId, title: boardTitle },
62
+ pushParent
63
+ });
64
+ render();
65
+ try {
66
+ const topics = asArray(await client.getBoardTopics(boardId, 0, 12, false, force, signal));
67
+ state.items = topics.map((topic) => topicItem(topic));
68
+ state.status = "版面帖子:j/k 选择 l 打开帖子 h 返回 r 刷新";
69
+ }
70
+ catch (error) {
71
+ if (isAbortError(error)) {
72
+ return;
73
+ }
74
+ state.error = error instanceof Error ? error.message : String(error);
75
+ }
76
+ finally {
77
+ state.loading = false;
78
+ render();
79
+ }
80
+ }
81
+ export async function openChat(client, state, userId, title, render, force = false, signal, pushParent = true) {
82
+ prepareListView(state, {
83
+ title,
84
+ status: "正在读取私信...",
85
+ currentChat: { userId, title, loaded: 0, size: 10, hasMore: true },
86
+ pushParent
87
+ });
88
+ const chat = state.currentChat;
89
+ if (!chat) {
90
+ return;
91
+ }
92
+ render();
93
+ try {
94
+ const messages = asArray(await client.getChatHistory(userId, 0, 10, force, signal));
95
+ state.items = chatMessageItems(messages, title, userId);
96
+ chat.loaded = messages.length;
97
+ chat.hasMore = messages.length === chat.size;
98
+ state.itemIndex = Math.max(0, state.items.length - 1);
99
+ state.status = chat.hasMore
100
+ ? "私信:j/k 滚动 n/Space 更早消息 Esc/Backspace 返回联系人 h 返回左栏"
101
+ : "私信:j/k 滚动 Esc/Backspace 返回联系人 h 返回左栏";
102
+ }
103
+ catch (error) {
104
+ if (isAbortError(error)) {
105
+ return;
106
+ }
107
+ state.error = error instanceof Error ? error.message : String(error);
108
+ state.status = "私信读取失败;Esc/Backspace 返回联系人 h 返回左栏 r 重试";
109
+ }
110
+ finally {
111
+ state.loading = false;
112
+ render();
113
+ }
114
+ }
115
+ export async function loadNextChatPage(client, state, render, signal) {
116
+ if (!state.currentChat || state.loadingMore || !state.currentChat.hasMore) {
117
+ return;
118
+ }
119
+ state.loadingMore = true;
120
+ state.status = "正在读取更早私信...";
121
+ render();
122
+ try {
123
+ const chat = state.currentChat;
124
+ const messages = asArray(await client.getChatHistory(chat.userId, chat.loaded, chat.size, false, signal));
125
+ const olderItems = chatMessageItems(messages, chat.title, chat.userId);
126
+ state.items = [...olderItems, ...state.items];
127
+ state.itemIndex += olderItems.length;
128
+ state.scroll += olderItems.length;
129
+ chat.loaded += messages.length;
130
+ chat.hasMore = messages.length === chat.size;
131
+ state.status = chat.hasMore
132
+ ? "私信:j/k 滚动 n/Space 更早消息 Esc/Backspace 返回联系人 h 返回左栏"
133
+ : "已到最早私信;j/k 滚动 Esc/Backspace 返回联系人 h 返回左栏";
134
+ }
135
+ catch (error) {
136
+ if (isAbortError(error)) {
137
+ return;
138
+ }
139
+ state.error = error instanceof Error ? error.message : String(error);
140
+ state.status = "更早私信读取失败;n/Space 重试 Esc/Backspace 返回联系人";
141
+ }
142
+ finally {
143
+ state.loadingMore = false;
144
+ render();
145
+ }
146
+ }
147
+ export function restoreParentList(state) {
148
+ if (!state.parentList) {
149
+ return;
150
+ }
151
+ applyListSnapshot(state, state.parentList);
152
+ state.parentList = undefined;
153
+ }
154
+ export async function loadNextTopicPage(client, state, render, config, signal, advanceAfterLoad = false) {
155
+ if (!state.topic || state.loadingMore || !state.topic.hasMore) {
156
+ return;
157
+ }
158
+ state.loadingMore = true;
159
+ state.status = "正在加载下一页...";
160
+ render();
161
+ try {
162
+ const posts = asArray(await client.getTopicPosts(state.topic.topicId, state.topic.loaded, state.topic.size, false, signal));
163
+ const next = renderPosts(posts, Math.max(36, currentTopicWidthEstimate(state.sidebarWidth)), config, state.topic.lines.length);
164
+ state.topic.lines.push(...next.lines);
165
+ state.topic.posts.push(...next.posts);
166
+ state.topic.imageCount += next.imageCount;
167
+ state.topic.linkCount += next.linkCount;
168
+ void loadTopicImagePreviews(state, render, config, state.sidebarWidth);
169
+ state.topic.loaded += posts.length;
170
+ state.topic.hasMore = posts.length === state.topic.size;
171
+ if (advanceAfterLoad && posts.length > 0) {
172
+ state.scroll = Math.min(Math.max(0, state.topic.lines.length - 1), state.scroll + 1);
173
+ }
174
+ }
175
+ catch (error) {
176
+ if (isAbortError(error)) {
177
+ return;
178
+ }
179
+ state.error = error instanceof Error ? error.message : String(error);
180
+ }
181
+ finally {
182
+ state.loadingMore = false;
183
+ if (!state.error && state.mode === "topic" && state.topic) {
184
+ state.status = getStatus(state);
185
+ }
186
+ render();
187
+ }
188
+ }
189
+ export async function jumpToTopicFloor(client, state, floor, render, config, signal) {
190
+ const topic = state.topic;
191
+ if (!topic) {
192
+ return;
193
+ }
194
+ const loaded = findTopicPostByFloor(topic, floor);
195
+ if (loaded) {
196
+ state.scroll = loaded.lineStart;
197
+ state.status = getStatus(state);
198
+ render();
199
+ return;
200
+ }
201
+ const from = Math.floor((floor - 1) / topic.size) * topic.size;
202
+ state.loadingMore = true;
203
+ state.status = `正在读取 ${floor} 楼...`;
204
+ render();
205
+ try {
206
+ const posts = asArray(await client.getTopicPosts(topic.topicId, from, topic.size, false, signal));
207
+ const next = renderPosts(posts, Math.max(36, currentTopicWidthEstimate(state.sidebarWidth)), config, topic.lines.length);
208
+ topic.lines.push(...next.lines);
209
+ topic.posts.push(...next.posts);
210
+ topic.posts.sort((left, right) => (left.floor ?? 0) - (right.floor ?? 0));
211
+ topic.imageCount += next.imageCount;
212
+ topic.linkCount += next.linkCount;
213
+ void loadTopicImagePreviews(state, render, config, state.sidebarWidth);
214
+ topic.loaded = Math.max(topic.loaded, from + posts.length);
215
+ topic.hasMore = posts.length === topic.size;
216
+ const target = findTopicPostByFloor(topic, floor);
217
+ if (target) {
218
+ state.scroll = target.lineStart;
219
+ }
220
+ else {
221
+ state.status = `未找到 ${floor} 楼`;
222
+ }
223
+ }
224
+ catch (error) {
225
+ if (!isAbortError(error)) {
226
+ state.error = error instanceof Error ? error.message : String(error);
227
+ }
228
+ }
229
+ finally {
230
+ state.loadingMore = false;
231
+ if (!state.error && findTopicPostByFloor(topic, floor)) {
232
+ state.status = getStatus(state);
233
+ }
234
+ render();
235
+ }
236
+ }
237
+ export function jumpRelativeTopicFloor(state, delta) {
238
+ const topic = state.topic;
239
+ if (!topic || topic.posts.length === 0) {
240
+ return;
241
+ }
242
+ const current = currentTopicPost(topic, state.scroll);
243
+ const currentIndex = current ? topic.posts.indexOf(current) : 0;
244
+ const next = topic.posts[Math.min(topic.posts.length - 1, Math.max(0, currentIndex + delta))];
245
+ if (next) {
246
+ state.scroll = next.lineStart;
247
+ }
248
+ }
249
+ export async function refreshAccounts(state, tokenStore) {
250
+ const accounts = await tokenStore.listAccounts();
251
+ const current = await tokenStore.getCurrentAccountName();
252
+ state.account = current;
253
+ state.accountModal.accounts = accounts.map((account) => ({
254
+ account: account.account,
255
+ detail: account.displayName ?? account.username ?? (account.userId ? `#${account.userId}` : "本地账号"),
256
+ isCurrent: account.account === current
257
+ }));
258
+ state.accountModal.selectedIndex = Math.min(state.accountModal.accounts.findIndex((account) => account.isCurrent), state.accountModal.accounts.length);
259
+ if (state.accountModal.selectedIndex < 0) {
260
+ state.accountModal.selectedIndex = 0;
261
+ }
262
+ }
263
+ export function getDefaultAccountName(me, username) {
264
+ if (typeof me.name === "string" && me.name.trim()) {
265
+ return me.name.trim();
266
+ }
267
+ if (typeof me.id === "number") {
268
+ return String(me.id);
269
+ }
270
+ return username;
271
+ }
272
+ export function normalizeLoginMessage(error) {
273
+ if (error instanceof Error) {
274
+ return error.message.replace(/^login failed:\s*/i, "");
275
+ }
276
+ return String(error);
277
+ }
278
+ export async function loadView(client, view, force, signal) {
279
+ switch (view) {
280
+ case "hot": {
281
+ const [index, unread] = await Promise.all([
282
+ client.getForumIndex(force, signal),
283
+ client.getUnreadCount(force, signal)
284
+ ]);
285
+ const indexObject = asObject(index);
286
+ const unreadObject = asObject(unread);
287
+ const hotTopics = asArray(indexObject.hotTopic ?? indexObject.manualHotTopic);
288
+ return {
289
+ title: "十大",
290
+ items: hotTopics.map((topic) => topicItem(topic)),
291
+ overview: overviewStats(indexObject, unreadObject)
292
+ };
293
+ }
294
+ case "new": {
295
+ const topics = asArray(await client.getNewTopics(0, 12, force, signal));
296
+ return {
297
+ title: "最新",
298
+ items: topics.map((topic) => topicItem(topic))
299
+ };
300
+ }
301
+ case "boards": {
302
+ const sections = asArray(await client.getAllBoards(force, signal));
303
+ const allBoards = flattenBoards(sections);
304
+ return {
305
+ title: "版面",
306
+ items: allBoards.slice(0, 14),
307
+ status: "版面:j/k 选择 l 进入版面 h 返回 r 刷新"
308
+ };
309
+ }
310
+ case "following": {
311
+ const topics = asArray(await client.getFolloweeTopics(0, 12, force, signal));
312
+ return {
313
+ title: "关注",
314
+ items: topics.map((topic) => topicItem(topic)),
315
+ status: "关注:j/k 选择 l 打开帖子 h 返回 r 刷新"
316
+ };
317
+ }
318
+ case "favorite": {
319
+ const [meRaw, sectionsRaw] = await Promise.all([
320
+ client.getMe(force, signal),
321
+ client.getAllBoards(false, signal)
322
+ ]);
323
+ const customBoards = asArray(asObject(meRaw).customBoards).filter((id) => typeof id === "number");
324
+ const allBoards = flattenBoards(asArray(sectionsRaw));
325
+ const boardById = new Map(allBoards.filter((board) => board.boardId !== undefined).map((board) => [board.boardId, board]));
326
+ const topicGroups = await mapLimit(customBoards, 3, async (boardId) => {
327
+ const board = boardById.get(boardId);
328
+ const topics = asArray(await client.getBoardTopics(boardId, 0, 3, false, force, signal));
329
+ return topics.map((topic) => topicItem(topic, board));
330
+ });
331
+ const items = topicGroups.flat().sort((left, right) => (right.sortTime ?? 0) - (left.sortTime ?? 0)).slice(0, 18);
332
+ return {
333
+ title: "收藏",
334
+ items,
335
+ status: "收藏:j/k 选择 l 打开帖子 h 返回 r 刷新"
336
+ };
337
+ }
338
+ case "messages": {
339
+ const [unread, recent] = await Promise.all([
340
+ client.getUnreadCount(force, signal),
341
+ client.getRecentChats(0, 10, force, signal)
342
+ ]);
343
+ const unreadObject = asObject(unread);
344
+ const unreadEntries = unreadStats(unreadObject);
345
+ const chats = asArray(recent);
346
+ const userNames = await loadChatUserNames(client, chats, force, signal);
347
+ const unreadItems = unreadEntries
348
+ .filter((entry) => entry.detail !== "0" && entry.detail !== "-")
349
+ .map((entry) => ({
350
+ title: `未读 ${entry.title}`,
351
+ detail: entry.detail
352
+ }));
353
+ const chatItems = chats.length > 0
354
+ ? chats.map((chat) => chatItem(chat, userNames))
355
+ : [{ title: "暂无最近私信", meta: "recent-contact-users" }];
356
+ return {
357
+ title: "消息",
358
+ items: [...unreadItems, ...chatItems],
359
+ status: "消息:j/k 选择 l 打开会话 h 返回 r 刷新"
360
+ };
361
+ }
362
+ case "me": {
363
+ const [me, cacheStats] = await Promise.all([
364
+ client.getMe(force, signal),
365
+ client.getCacheStats()
366
+ ]);
367
+ const meObject = asObject(me);
368
+ return {
369
+ title: "我的",
370
+ items: [
371
+ item("昵称", meObject.name),
372
+ item("用户 ID", meObject.id),
373
+ item("等级", meObject.levelTitle ?? meObject.groupName),
374
+ item("发帖数", meObject.postCount),
375
+ item("财富", meObject.wealth),
376
+ item("关注", meObject.followCount),
377
+ item("粉丝", meObject.fanCount),
378
+ item("缓存文件", cacheStats.fileCacheEntries)
379
+ ]
380
+ };
381
+ }
382
+ case "settings": {
383
+ return {
384
+ title: "设置",
385
+ items: settingsItems,
386
+ status: "设置:j/k 选择 l 执行 h 返回"
387
+ };
388
+ }
389
+ }
390
+ }
391
+ function currentTopicWidthEstimate(sidebarWidthOverride) {
392
+ const width = process.stdout.columns || Number(process.env.COLUMNS) || 80;
393
+ const sidebarWidth = getSidebarWidth(width, sidebarWidthOverride);
394
+ const sidebarRuleWidth = sidebarWidth > 0 ? 1 : 0;
395
+ return Math.max(24, width - sidebarWidth - sidebarRuleWidth);
396
+ }
397
+ function snapshotCurrentList(state) {
398
+ return {
399
+ title: state.viewTitle,
400
+ items: state.items,
401
+ itemIndex: state.itemIndex,
402
+ status: state.status
403
+ };
404
+ }
405
+ function prepareListView(state, options) {
406
+ if (options.pushParent) {
407
+ state.parentList = snapshotCurrentList(state);
408
+ }
409
+ state.mode = "list";
410
+ state.focus = "content";
411
+ state.loading = true;
412
+ state.loadingMore = false;
413
+ state.error = undefined;
414
+ state.itemIndex = 0;
415
+ state.scroll = 0;
416
+ state.topic = undefined;
417
+ state.imageViewer = undefined;
418
+ state.currentBoard = options.currentBoard;
419
+ state.currentChat = options.currentChat;
420
+ state.viewTitle = options.title;
421
+ state.items = [];
422
+ state.status = options.status;
423
+ }
424
+ function applyListSnapshot(state, snapshot) {
425
+ state.mode = "list";
426
+ state.focus = "content";
427
+ state.loading = false;
428
+ state.loadingMore = false;
429
+ state.error = undefined;
430
+ state.topic = undefined;
431
+ state.imageViewer = undefined;
432
+ state.currentBoard = undefined;
433
+ state.currentChat = undefined;
434
+ state.viewTitle = snapshot.title;
435
+ state.items = snapshot.items;
436
+ state.itemIndex = snapshot.itemIndex;
437
+ state.status = snapshot.status;
438
+ }
439
+ async function loadTopicImagePreviews(state, render, config, sidebarWidthOverride) {
440
+ const topic = state.topic;
441
+ if (!topic || !config.previewImages) {
442
+ return;
443
+ }
444
+ const width = Math.max(12, currentTopicWidthEstimate(sidebarWidthOverride) - 4);
445
+ const maxRows = imagePreviewRows;
446
+ const imageLines = topic.posts
447
+ .flatMap((post) => post.lines)
448
+ .filter((line) => line.kind === "image" && line.imageUrl);
449
+ const previewEnabled = supportsImagePreview();
450
+ for (const line of imageLines) {
451
+ try {
452
+ if (state.topic !== topic) {
453
+ return;
454
+ }
455
+ if (!line.imagePreviewRows) {
456
+ const measured = await measureImagePreview(line.imageUrl ?? "", width, maxRows);
457
+ if (state.topic !== topic) {
458
+ return;
459
+ }
460
+ if (measured) {
461
+ line.imagePreviewRows = measured.rows;
462
+ adjustTopicImageBlockHeight(topic, line, measured.rows, state);
463
+ render();
464
+ }
465
+ }
466
+ if (!previewEnabled || line.imagePreview) {
467
+ continue;
468
+ }
469
+ const rows = Math.max(1, Math.min(maxRows, line.imagePreviewRows ?? line.imageBlockRows ?? imagePreviewRows));
470
+ const preview = await loadImagePreview(line.imageUrl ?? "", width, rows);
471
+ if (state.topic !== topic) {
472
+ return;
473
+ }
474
+ if (preview) {
475
+ line.imagePreview = preview.token;
476
+ line.imagePreviewRows = preview.size.rows;
477
+ adjustTopicImageBlockHeight(topic, line, preview.size.rows, state);
478
+ render();
479
+ }
480
+ }
481
+ catch {
482
+ // Keep the textual image placeholder if preview loading fails.
483
+ }
484
+ }
485
+ }
486
+ function buildTopicReader(topicId, topic, posts, size, config) {
487
+ const title = normalizeInlineText(String(topic.title ?? `#${topicId}`));
488
+ const meta = [
489
+ topic.userName,
490
+ topic.replyCount !== undefined ? `${topic.replyCount} 回复` : undefined,
491
+ topic.hitCount !== undefined ? `${topic.hitCount} 浏览` : undefined
492
+ ].filter(Boolean).join(" · ");
493
+ const rendered = renderPosts(posts, currentTopicWidthEstimate(), config);
494
+ return {
495
+ topicId,
496
+ title,
497
+ meta,
498
+ lines: rendered.lines,
499
+ posts: rendered.posts,
500
+ loaded: posts.length,
501
+ size,
502
+ hasMore: posts.length === size,
503
+ imageCount: rendered.imageCount,
504
+ linkCount: rendered.linkCount,
505
+ floorInput: ""
506
+ };
507
+ }
508
+ function renderPosts(posts, width, config, lineOffset = 0) {
509
+ const lines = [];
510
+ const entries = [];
511
+ let imageCount = 0;
512
+ let linkCount = 0;
513
+ posts.forEach((postRaw) => {
514
+ const post = asObject(postRaw);
515
+ const lineStart = lineOffset + lines.length;
516
+ const postLines = [];
517
+ const floorNumber = asNumber(post.floor);
518
+ const floor = floorNumber !== undefined ? `#${floorNumber}` : "#?";
519
+ const author = String(post.userName ?? "匿名");
520
+ const time = typeof post.time === "string" ? post.time.replace("T", " ").slice(0, 16) : "";
521
+ const likeCount = asNumber(post.likeCount) ?? 0;
522
+ const dislikeCount = asNumber(post.dislikeCount) ?? 0;
523
+ const reactions = ` · ${likeCount} 赞 · ${dislikeCount} 踩`;
524
+ const push = (text, kind, extra = {}) => {
525
+ const line = lineOffset + lines.length;
526
+ lines.push(text);
527
+ postLines.push({
528
+ line,
529
+ row: postLines.length,
530
+ floor: floorNumber,
531
+ kind,
532
+ text,
533
+ ...extra
534
+ });
535
+ };
536
+ push(`${floor} ${author}${time ? ` · ${time}` : ""}${reactions}`, "header");
537
+ const contentWidth = Math.max(8, width - 2);
538
+ push(theme.border.horizontal.repeat(contentWidth), "divider");
539
+ const content = typeof post.content === "string" ? post.content : "";
540
+ const contentType = asNumber(post.contentType) ?? 0;
541
+ const rendered = contentType === 1
542
+ ? renderMarkdownToLines(content, contentWidth, {
543
+ imagePreviewRows: config.previewImages ? imagePreviewRows : 0
544
+ })
545
+ : renderUbbToLines(content, contentWidth, {
546
+ imagePreviewRows: config.previewImages ? imagePreviewRows : 0
547
+ });
548
+ rendered.lines.forEach((renderedLine, index) => {
549
+ const imageIndex = parseBracketIndex(renderedLine, "image");
550
+ const linkIndex = parseBracketIndex(renderedLine, "link");
551
+ const imageBlockRows = imageIndex !== undefined ? imageBlockHeight(rendered.lines, index) : undefined;
552
+ const kind = renderedLine.trim() === ""
553
+ ? "blank"
554
+ : imageIndex !== undefined
555
+ ? "image"
556
+ : linkIndex !== undefined
557
+ ? "link"
558
+ : renderedLine.startsWith(theme.quote.prefix)
559
+ ? "quote"
560
+ : "text";
561
+ push(renderedLine, kind, {
562
+ imageIndex,
563
+ imageUrl: imageIndex !== undefined ? rendered.images[imageIndex - 1] : undefined,
564
+ imageBlockRows,
565
+ linkIndex,
566
+ linkUrl: linkIndex !== undefined ? rendered.links[linkIndex - 1] : undefined
567
+ });
568
+ });
569
+ push("", "blank");
570
+ const preview = rendered.lines.find((value) => value.trim() &&
571
+ !value.startsWith("[image ") &&
572
+ !value.startsWith("[link ")) ?? "";
573
+ entries.push({
574
+ id: asNumber(post.id),
575
+ floor: floorNumber,
576
+ author,
577
+ time,
578
+ likeCount,
579
+ dislikeCount,
580
+ rating: formatRating(post),
581
+ preview,
582
+ lineStart,
583
+ lineEnd: lineOffset + lines.length - 1,
584
+ imageCount: rendered.images.length,
585
+ linkCount: rendered.links.length,
586
+ images: rendered.images,
587
+ links: rendered.links,
588
+ lines: postLines
589
+ });
590
+ imageCount += rendered.images.length;
591
+ linkCount += rendered.links.length;
592
+ });
593
+ return { lines, posts: entries, imageCount, linkCount };
594
+ }
595
+ function findTopicPostByFloor(topic, floor) {
596
+ return topic.posts.find((entry) => entry.floor === floor);
597
+ }
598
+ function imageBlockHeight(lines, start) {
599
+ let height = 1;
600
+ for (let index = start + 1; index < lines.length; index += 1) {
601
+ const line = lines[index] ?? "";
602
+ if (line.startsWith("[image ") || line.trim() !== "") {
603
+ break;
604
+ }
605
+ height += 1;
606
+ }
607
+ return height;
608
+ }
609
+ function adjustTopicImageBlockHeight(topic, line, nextRows, state) {
610
+ const post = topic.posts.find((entry) => entry.lines.includes(line));
611
+ if (!post) {
612
+ return;
613
+ }
614
+ const startRow = line.row;
615
+ const currentRows = Math.max(1, line.imageBlockRows ?? 1);
616
+ const targetRows = Math.max(1, nextRows);
617
+ if (currentRows === targetRows) {
618
+ line.imageBlockRows = targetRows;
619
+ return;
620
+ }
621
+ const originalLine = line.line;
622
+ const removeCount = Math.min(currentRows, Math.max(1, post.lines.length - startRow));
623
+ const delta = targetRows - currentRows;
624
+ const filler = Array.from({ length: targetRows - 1 }, () => ({
625
+ line: 0,
626
+ row: 0,
627
+ floor: line.floor,
628
+ kind: "blank",
629
+ text: ""
630
+ }));
631
+ line.imageBlockRows = targetRows;
632
+ post.lines.splice(startRow, removeCount, line, ...filler);
633
+ rebuildTopicLines(topic);
634
+ if (originalLine < state.scroll) {
635
+ state.scroll = Math.max(0, state.scroll + delta);
636
+ }
637
+ }
638
+ function rebuildTopicLines(topic) {
639
+ const lines = [];
640
+ topic.posts.forEach((post) => {
641
+ post.lineStart = lines.length;
642
+ post.lines.forEach((entry, row) => {
643
+ entry.row = row;
644
+ entry.line = lines.length;
645
+ lines.push(entry.text);
646
+ });
647
+ post.lineEnd = Math.max(post.lineStart, lines.length - 1);
648
+ });
649
+ topic.lines = lines;
650
+ }
651
+ function parseBracketIndex(value, label) {
652
+ const match = new RegExp(`\\[${label} (\\d+)`).exec(value);
653
+ return match ? Number(match[1]) : undefined;
654
+ }
655
+ function formatRating(post) {
656
+ const value = post.rating ?? post.ratingCount ?? post.wealth ?? post.score;
657
+ if (typeof value === "number" && Number.isFinite(value)) {
658
+ return String(value);
659
+ }
660
+ if (typeof value === "string" && value.trim()) {
661
+ return value.trim();
662
+ }
663
+ return undefined;
664
+ }
665
+ function item(title, value, meta) {
666
+ return {
667
+ title,
668
+ meta,
669
+ detail: value === undefined || value === null ? "-" : String(value)
670
+ };
671
+ }
672
+ function topicItem(value, fallbackBoard) {
673
+ const topic = asObject(value);
674
+ const topicId = asNumber(topic.id ?? topic.Id);
675
+ const boardId = asNumber(topic.boardId ?? topic.BoardId) ?? fallbackBoard?.boardId;
676
+ const boardName = topic.boardName ?? topic.BoardName ?? fallbackBoard?.title;
677
+ const authorName = normalizeInlineText(String(topic.userName ?? topic.authorName ?? "")).trim() || "匿名";
678
+ return {
679
+ title: normalizeInlineText(String(topic.title ?? topic.Title ?? `#${topicId ?? ""}`)),
680
+ meta: [
681
+ boardName,
682
+ authorName,
683
+ topic.replyCount !== undefined ? `${topic.replyCount} 回复` : undefined,
684
+ topic.hitCount !== undefined ? `${topic.hitCount} 浏览` : undefined
685
+ ]
686
+ .filter(Boolean)
687
+ .join(" · "),
688
+ detail: typeof topic.lastPostContent === "string" ? topic.lastPostContent.replace(/\s+/g, " ") : undefined,
689
+ topicId,
690
+ boardId,
691
+ sortTime: timestampOf(topic.lastPostTime ?? topic.updateTime ?? topic.time ?? topic.createTime)
692
+ };
693
+ }
694
+ async function loadChatUserNames(client, chats, force, signal) {
695
+ const ids = chats
696
+ .map((chat) => asNumber(asObject(chat).userId ?? asObject(chat).UserId))
697
+ .filter((id) => id !== undefined);
698
+ const users = asArray(await client.getBasicUsers(ids, force, signal));
699
+ return new Map(users.map((userRaw) => {
700
+ const user = asObject(userRaw);
701
+ const id = asNumber(user.id ?? user.Id);
702
+ const name = String(user.name ?? user.Name ?? (id !== undefined ? `#${id}` : "用户"));
703
+ return [id, name];
704
+ }).filter((entry) => entry[0] !== undefined));
705
+ }
706
+ function chatItem(value, userNames) {
707
+ const chat = asObject(value);
708
+ const userId = asNumber(chat.userId ?? chat.UserId);
709
+ const name = userId !== undefined ? userNames.get(userId) : undefined;
710
+ return {
711
+ title: String(name ?? chat.name ?? chat.userName ?? userId ?? "私信"),
712
+ meta: userId !== undefined ? `user #${userId}` : undefined,
713
+ detail: normalizePreview(String(chat.lastContent ?? chat.lastMessage ?? chat.content ?? "")),
714
+ chatUserId: userId
715
+ };
716
+ }
717
+ function chatMessageItems(messages, otherName, otherUserId) {
718
+ return [...messages].reverse().map((messageRaw) => {
719
+ const message = asObject(messageRaw);
720
+ const receiverId = asNumber(message.receiverId ?? message.ReceiverId);
721
+ const isMine = receiverId === otherUserId;
722
+ const time = typeof message.time === "string"
723
+ ? message.time.replace("T", " ").slice(0, 16)
724
+ : "";
725
+ const content = normalizePreview(String(message.content ?? message.Content ?? ""));
726
+ return {
727
+ title: isMine ? `我 -> ${otherName}` : `${otherName} -> 我`,
728
+ meta: [time, receiverId !== undefined ? `receiver #${receiverId}` : undefined].filter(Boolean).join(" · "),
729
+ detail: content || "(空消息)"
730
+ };
731
+ });
732
+ }
733
+ function unreadStats(value) {
734
+ return [
735
+ item("系统", value.systemCount),
736
+ item("@", value.atCount),
737
+ item("回复", value.replyCount),
738
+ item("私信", value.messageCount)
739
+ ];
740
+ }
741
+ function overviewStats(index, unread) {
742
+ const unreadTotal = ["systemCount", "atCount", "replyCount", "messageCount"].reduce((total, key) => {
743
+ const value = unread[key];
744
+ return total + (typeof value === "number" ? value : 0);
745
+ }, 0);
746
+ return [
747
+ item("今日主题", index.todayTopicCount),
748
+ item("今日回复", index.todayCount),
749
+ item("在线", index.onlineUserCount),
750
+ item("用户", index.userCount),
751
+ item("未读", unreadTotal)
752
+ ];
753
+ }
754
+ async function mapLimit(values, limit, mapper) {
755
+ const results = [];
756
+ let nextIndex = 0;
757
+ const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
758
+ while (nextIndex < values.length) {
759
+ const index = nextIndex;
760
+ nextIndex += 1;
761
+ results[index] = await mapper(values[index]);
762
+ }
763
+ });
764
+ await Promise.all(workers);
765
+ return results;
766
+ }
767
+ function flattenBoards(sections) {
768
+ const boards = [];
769
+ for (const section of sections) {
770
+ const sectionObject = asObject(section);
771
+ const sectionName = String(sectionObject.name ?? sectionObject.title ?? "分区");
772
+ const candidates = [sectionObject.boards, sectionObject.children, sectionObject.boardList];
773
+ for (const candidate of candidates) {
774
+ if (!Array.isArray(candidate)) {
775
+ continue;
776
+ }
777
+ for (const board of candidate) {
778
+ const boardObject = asObject(board);
779
+ boards.push({
780
+ title: String(boardObject.name ?? boardObject.title ?? `#${boardObject.id ?? ""}`),
781
+ meta: `${sectionName}${boardObject.id !== undefined ? ` · #${boardObject.id}` : ""}`,
782
+ detail: typeof boardObject.description === "string" ? boardObject.description : undefined,
783
+ boardId: typeof boardObject.id === "number" ? boardObject.id : undefined
784
+ });
785
+ }
786
+ }
787
+ }
788
+ return boards;
789
+ }
790
+ function asObject(value) {
791
+ return typeof value === "object" && value !== null ? value : {};
792
+ }
793
+ function asArray(value) {
794
+ return Array.isArray(value) ? value : [];
795
+ }
796
+ function asNumber(value) {
797
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
798
+ }
799
+ function normalizeInline(value) {
800
+ return value.replace(/\s+/g, " ").trim();
801
+ }
802
+ function normalizePreview(value) {
803
+ return normalizeInline(value
804
+ .replace(/\[img\][\s\S]*?\[\/img\]/gi, " [图片] ")
805
+ .replace(/\[upload(?:=[^\]]*)?\][\s\S]*?\[\/upload\]/gi, " [附件] ")
806
+ .replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi, (_match, _url, label) => ` ${label} `)
807
+ .replace(/\[url\][\s\S]*?\[\/url\]/gi, " [链接] ")
808
+ .replace(/<img\b[^>]*>/gi, " [图片] ")
809
+ .replace(/<br\s*\/?>/gi, " ")
810
+ .replace(/<\/?[^>]+>/g, " ")
811
+ .replace(/\[(?:\/)?(?:b|i|u|size|color|align|email|del|s|sub|sup|h\d?|quote|code)(?:=[^\]]*)?\]/gi, "")
812
+ .replace(/\[[a-z0-9]+(?:=[^\]]*)?\]/gi, " ")
813
+ .replace(/\[\/[a-z0-9]+\]/gi, " "));
814
+ }
815
+ function timestampOf(value) {
816
+ if (typeof value !== "string" && typeof value !== "number") {
817
+ return undefined;
818
+ }
819
+ const timestamp = new Date(value).getTime();
820
+ return Number.isFinite(timestamp) ? timestamp : undefined;
821
+ }
822
+ function normalizeInlineText(value) {
823
+ return value.replace(/\s+/g, " ").trim();
824
+ }
825
+ function isAbortError(error) {
826
+ return error instanceof Error && error.name === "AbortError";
827
+ }
828
+ //# sourceMappingURL=app-data.js.map