cc98-cli 0.4.0 → 0.5.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.
- package/CHANGELOG.md +14 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +49 -3
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/storage/settings-store.d.ts +12 -0
- package/dist/storage/settings-store.d.ts.map +1 -0
- package/dist/storage/settings-store.js +46 -0
- package/dist/storage/settings-store.js.map +1 -0
- package/dist/tui/ansi.d.ts.map +1 -1
- package/dist/tui/ansi.js +5 -1
- package/dist/tui/ansi.js.map +1 -1
- package/dist/tui/app.js +1 -1
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/content.d.ts +2 -0
- package/dist/tui/components/content.d.ts.map +1 -1
- package/dist/tui/components/content.js +47 -9
- package/dist/tui/components/content.js.map +1 -1
- package/dist/tui/components/status.d.ts +3 -0
- package/dist/tui/components/status.d.ts.map +1 -1
- package/dist/tui/components/status.js +21 -8
- package/dist/tui/components/status.js.map +1 -1
- package/dist/tui/components/utils.d.ts.map +1 -1
- package/dist/tui/components/utils.js +33 -12
- package/dist/tui/components/utils.js.map +1 -1
- package/dist/tui/controller.d.ts +36 -1
- package/dist/tui/controller.d.ts.map +1 -1
- package/dist/tui/controller.js +802 -124
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/emoji-art.d.ts +11 -0
- package/dist/tui/emoji-art.d.ts.map +1 -0
- package/dist/tui/emoji-art.js +371 -0
- package/dist/tui/emoji-art.js.map +1 -0
- package/dist/tui/emoji-renderer.d.ts +16 -0
- package/dist/tui/emoji-renderer.d.ts.map +1 -0
- package/dist/tui/emoji-renderer.js +110 -0
- package/dist/tui/emoji-renderer.js.map +1 -0
- package/dist/tui/image-renderer.d.ts +46 -0
- package/dist/tui/image-renderer.d.ts.map +1 -0
- package/dist/tui/image-renderer.js +259 -0
- package/dist/tui/image-renderer.js.map +1 -0
- package/dist/tui/keybindings.d.ts +24 -0
- package/dist/tui/keybindings.d.ts.map +1 -0
- package/dist/tui/keybindings.js +207 -0
- package/dist/tui/keybindings.js.map +1 -0
- package/dist/tui/navigation.d.ts.map +1 -1
- package/dist/tui/navigation.js +4 -0
- package/dist/tui/navigation.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +181 -11
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/state/store.d.ts.map +1 -1
- package/dist/tui/state/store.js +13 -2
- package/dist/tui/state/store.js.map +1 -1
- package/dist/tui/state/types.d.ts +24 -0
- package/dist/tui/state/types.d.ts.map +1 -1
- package/dist/tui/terminal-capabilities.d.ts +24 -0
- package/dist/tui/terminal-capabilities.d.ts.map +1 -0
- package/dist/tui/terminal-capabilities.js +55 -0
- package/dist/tui/terminal-capabilities.js.map +1 -0
- package/dist/tui/terminal.js +1 -1
- package/dist/tui/terminal.js.map +1 -1
- package/dist/tui/topic-reader.d.ts +9 -1
- package/dist/tui/topic-reader.d.ts.map +1 -1
- package/dist/tui/topic-reader.js +30 -14
- package/dist/tui/topic-reader.js.map +1 -1
- package/dist/tui/ubb-renderer.d.ts +5 -1
- package/dist/tui/ubb-renderer.d.ts.map +1 -1
- package/dist/tui/ubb-renderer.js +71 -25
- package/dist/tui/ubb-renderer.js.map +1 -1
- package/dist/update.d.ts +4 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +71 -11
- package/dist/update.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -2
- package/dist/version.js.map +1 -1
- package/package.json +2 -1
- package/dist/tui/components/layout.d.ts +0 -3
- package/dist/tui/components/layout.d.ts.map +0 -1
- package/dist/tui/components/layout.js +0 -453
- package/dist/tui/components/layout.js.map +0 -1
package/dist/tui/controller.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { checkForUpdate } from "../update.js";
|
|
2
2
|
import { appVersion } from "../version.js";
|
|
3
|
+
import { Cc98Client } from "../api/client.js";
|
|
3
4
|
import { getImageCache } from "../storage/image-cache.js";
|
|
5
|
+
import { SettingsStore } from "../storage/settings-store.js";
|
|
6
|
+
import { getKeybindingManager } from "./keybindings.js";
|
|
7
|
+
import { EMOJI_CATEGORIES, getEmojiArt, renderCc98Logo, renderEmojiCode } from "./emoji-renderer.js";
|
|
4
8
|
import { navItems, settingsItems } from "./navigation.js";
|
|
5
9
|
import { getStatus } from "./state/store.js";
|
|
6
10
|
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,
|
|
11
|
+
import { appendTopicPosts, buildTopicReader, currentTopicPost, findTopicPostByFloor, getTopicPageInfo, jumpToPage, FLOORS_PER_PAGE } from "./topic-reader.js";
|
|
8
12
|
export class TuiController {
|
|
9
13
|
state;
|
|
10
14
|
client;
|
|
@@ -13,8 +17,13 @@ export class TuiController {
|
|
|
13
17
|
close;
|
|
14
18
|
nextSignal;
|
|
15
19
|
abortCurrent;
|
|
20
|
+
webVpnOptions;
|
|
16
21
|
loadVersion = 0;
|
|
17
|
-
|
|
22
|
+
keybindings;
|
|
23
|
+
settingsStore = new SettingsStore();
|
|
24
|
+
updateChecked = false;
|
|
25
|
+
autoSigninChecked = false;
|
|
26
|
+
constructor(state, client, tokenStore, render, close, nextSignal, abortCurrent, webVpnOptions) {
|
|
18
27
|
this.state = state;
|
|
19
28
|
this.client = client;
|
|
20
29
|
this.tokenStore = tokenStore;
|
|
@@ -22,9 +31,12 @@ export class TuiController {
|
|
|
22
31
|
this.close = close;
|
|
23
32
|
this.nextSignal = nextSignal;
|
|
24
33
|
this.abortCurrent = abortCurrent;
|
|
34
|
+
this.webVpnOptions = webVpnOptions;
|
|
35
|
+
this.keybindings = getKeybindingManager();
|
|
25
36
|
}
|
|
26
37
|
async load(force = false) {
|
|
27
38
|
const version = ++this.loadVersion;
|
|
39
|
+
let shouldAutoSignin = false;
|
|
28
40
|
const signal = this.nextSignal();
|
|
29
41
|
const nav = navItems[this.state.navIndex] ?? navItems[0];
|
|
30
42
|
this.state.viewTitle = nav.label;
|
|
@@ -44,7 +56,18 @@ export class TuiController {
|
|
|
44
56
|
this.state.currentChat = undefined;
|
|
45
57
|
this.render();
|
|
46
58
|
try {
|
|
59
|
+
// 加载快捷键配置
|
|
60
|
+
await this.keybindings.load();
|
|
47
61
|
this.state.account = await this.tokenStore.getCurrentAccountName();
|
|
62
|
+
// 异步检查更新(不阻塞主加载)
|
|
63
|
+
if (!this.updateChecked) {
|
|
64
|
+
this.updateChecked = true;
|
|
65
|
+
void this.checkUpdate();
|
|
66
|
+
}
|
|
67
|
+
if (!this.autoSigninChecked) {
|
|
68
|
+
this.autoSigninChecked = true;
|
|
69
|
+
shouldAutoSignin = true;
|
|
70
|
+
}
|
|
48
71
|
const next = await this.loadView(nav.id, force, signal);
|
|
49
72
|
if (version !== this.loadVersion)
|
|
50
73
|
return;
|
|
@@ -67,6 +90,9 @@ export class TuiController {
|
|
|
67
90
|
if (version === this.loadVersion) {
|
|
68
91
|
this.state.loading = false;
|
|
69
92
|
this.render();
|
|
93
|
+
if (shouldAutoSignin) {
|
|
94
|
+
void this.runAutoSignin();
|
|
95
|
+
}
|
|
70
96
|
}
|
|
71
97
|
}
|
|
72
98
|
}
|
|
@@ -75,11 +101,20 @@ export class TuiController {
|
|
|
75
101
|
this.handleInputKey(key);
|
|
76
102
|
return;
|
|
77
103
|
}
|
|
78
|
-
|
|
104
|
+
// 关闭更新通知(Esc 或任意键)
|
|
105
|
+
if (this.state.updateAvailable?.isNew) {
|
|
106
|
+
if (key === "\x1b" || key === "\r") {
|
|
107
|
+
this.dismissUpdate();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// 其他按键也关闭更新通知,继续处理原按键动作
|
|
111
|
+
this.dismissUpdate();
|
|
112
|
+
}
|
|
113
|
+
if (this.keybindings.matches(key, "quit")) {
|
|
79
114
|
this.close();
|
|
80
115
|
return;
|
|
81
116
|
}
|
|
82
|
-
if (key
|
|
117
|
+
if (this.keybindings.matches(key, "help")) {
|
|
83
118
|
this.state.modal = this.state.modal === "help" ? null : "help";
|
|
84
119
|
this.render();
|
|
85
120
|
return;
|
|
@@ -103,17 +138,17 @@ export class TuiController {
|
|
|
103
138
|
this.handleContentKey(key);
|
|
104
139
|
}
|
|
105
140
|
handleInputKey(key) {
|
|
106
|
-
if (key
|
|
141
|
+
if (this.keybindings.matches(key, "inputCancel")) {
|
|
107
142
|
this.state.inputMode = false;
|
|
108
143
|
this.state.inputValue = "";
|
|
109
144
|
this.render();
|
|
110
145
|
return;
|
|
111
146
|
}
|
|
112
|
-
if (key
|
|
147
|
+
if (this.keybindings.matches(key, "inputConfirm")) {
|
|
113
148
|
this.state.inputCallback?.(this.state.inputValue);
|
|
114
149
|
return;
|
|
115
150
|
}
|
|
116
|
-
if (key
|
|
151
|
+
if (this.keybindings.matches(key, "inputBackspace")) {
|
|
117
152
|
this.state.inputValue = this.state.inputValue.slice(0, -1);
|
|
118
153
|
this.render();
|
|
119
154
|
return;
|
|
@@ -124,8 +159,19 @@ export class TuiController {
|
|
|
124
159
|
}
|
|
125
160
|
}
|
|
126
161
|
handleModalKey(key) {
|
|
127
|
-
if (this.state.modal === "help"
|
|
128
|
-
|
|
162
|
+
if (this.state.modal === "help") {
|
|
163
|
+
this.closeModal();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.state.modal === "info") {
|
|
167
|
+
// 如果有确认回调,确认键执行回调,其它键关闭。
|
|
168
|
+
if (this.state.confirmCallback && this.keybindings.matches(key, "confirm")) {
|
|
169
|
+
const callback = this.state.confirmCallback;
|
|
170
|
+
this.state.confirmCallback = undefined;
|
|
171
|
+
this.closeModal();
|
|
172
|
+
callback();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
129
175
|
this.closeModal();
|
|
130
176
|
return;
|
|
131
177
|
}
|
|
@@ -142,31 +188,30 @@ export class TuiController {
|
|
|
142
188
|
}
|
|
143
189
|
}
|
|
144
190
|
handleSearchKey(key) {
|
|
145
|
-
if (key
|
|
146
|
-
// Esc 或 / 关闭搜索
|
|
191
|
+
if (this.keybindings.matches(key, "searchClose")) {
|
|
147
192
|
this.state.modal = null;
|
|
148
193
|
this.state.searchQuery = "";
|
|
149
194
|
this.render();
|
|
150
195
|
return;
|
|
151
196
|
}
|
|
152
|
-
if (key
|
|
197
|
+
if (this.keybindings.matches(key, "searchToggleMode")) {
|
|
153
198
|
this.state.searchMode = this.state.searchMode === "topics" ? "users" : "topics";
|
|
154
199
|
this.state.searchResults = [];
|
|
155
200
|
this.state.itemIndex = 0;
|
|
156
201
|
this.render();
|
|
157
202
|
return;
|
|
158
203
|
}
|
|
159
|
-
if ((key
|
|
204
|
+
if (this.keybindings.matches(key, "searchNext") && this.state.searchResults.length > 0) {
|
|
160
205
|
this.state.itemIndex = Math.min(this.state.searchResults.length - 1, this.state.itemIndex + 1);
|
|
161
206
|
this.render();
|
|
162
207
|
return;
|
|
163
208
|
}
|
|
164
|
-
if ((key
|
|
209
|
+
if (this.keybindings.matches(key, "searchPrev") && this.state.searchResults.length > 0) {
|
|
165
210
|
this.state.itemIndex = Math.max(0, this.state.itemIndex - 1);
|
|
166
211
|
this.render();
|
|
167
212
|
return;
|
|
168
213
|
}
|
|
169
|
-
if (key
|
|
214
|
+
if (this.keybindings.matches(key, "searchExecute")) {
|
|
170
215
|
const selected = this.state.searchResults[this.state.itemIndex];
|
|
171
216
|
if (selected) {
|
|
172
217
|
// 有选中项:打开
|
|
@@ -222,23 +267,22 @@ export class TuiController {
|
|
|
222
267
|
}
|
|
223
268
|
}
|
|
224
269
|
handleMenuKey(key) {
|
|
225
|
-
if (key
|
|
270
|
+
if (this.keybindings.matches(key, "menuNext")) {
|
|
226
271
|
this.state.menuIndex = Math.min(Math.max(0, this.state.menuItems.length - 1), this.state.menuIndex + 1);
|
|
227
272
|
this.render();
|
|
228
273
|
return;
|
|
229
274
|
}
|
|
230
|
-
if (key
|
|
275
|
+
if (this.keybindings.matches(key, "menuPrev")) {
|
|
231
276
|
this.state.menuIndex = Math.max(0, this.state.menuIndex - 1);
|
|
232
277
|
this.render();
|
|
233
278
|
return;
|
|
234
279
|
}
|
|
235
|
-
if (key
|
|
236
|
-
// Esc 或 o 关闭菜单
|
|
280
|
+
if (this.keybindings.matches(key, "menuClose")) {
|
|
237
281
|
this.state.modal = null;
|
|
238
282
|
this.render();
|
|
239
283
|
return;
|
|
240
284
|
}
|
|
241
|
-
if (key
|
|
285
|
+
if (this.keybindings.matches(key, "menuExecute")) {
|
|
242
286
|
// Enter 或 l 执行选中项
|
|
243
287
|
const selected = this.state.menuItems[this.state.menuIndex];
|
|
244
288
|
this.state.modal = null;
|
|
@@ -250,84 +294,137 @@ export class TuiController {
|
|
|
250
294
|
}
|
|
251
295
|
}
|
|
252
296
|
handleTopicKey(key) {
|
|
297
|
+
// 数字输入:收集跳转目标
|
|
253
298
|
if (/^\d$/.test(key) && this.state.topic) {
|
|
254
299
|
this.state.topic.floorInput = `${this.state.topic.floorInput}${key}`.slice(0, 6);
|
|
255
|
-
this.state.status =
|
|
300
|
+
this.state.status = `输入: ${this.state.topic.floorInput}`;
|
|
256
301
|
this.render();
|
|
257
302
|
return;
|
|
258
303
|
}
|
|
259
|
-
|
|
304
|
+
// 退格:删除输入
|
|
305
|
+
if (this.keybindings.matches(key, "inputBackspace") && this.state.topic?.floorInput) {
|
|
260
306
|
this.state.topic.floorInput = this.state.topic.floorInput.slice(0, -1);
|
|
261
|
-
this.state.status = this.state.topic.floorInput ?
|
|
307
|
+
this.state.status = this.state.topic.floorInput ? `输入: ${this.state.topic.floorInput}` : "";
|
|
262
308
|
this.render();
|
|
263
309
|
return;
|
|
264
310
|
}
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
// 数字 + 跳页键:跳页
|
|
312
|
+
if (this.keybindings.matches(key, "topicJumpPage") && this.state.topic?.floorInput) {
|
|
313
|
+
const page = Number(this.state.topic.floorInput);
|
|
314
|
+
this.state.topic.jumpTarget = { type: "page", value: page };
|
|
267
315
|
this.state.topic.floorInput = "";
|
|
268
|
-
|
|
269
|
-
|
|
316
|
+
this.state.status = `跳转到第 ${page} 页?${this.keybindings.formatActionKeys("confirm")} 确认 ${this.keybindings.formatActionKeys("back")} 取消`;
|
|
317
|
+
this.render();
|
|
270
318
|
return;
|
|
271
319
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
320
|
+
// 数字 + 跳楼键:跳楼
|
|
321
|
+
if (this.keybindings.matches(key, "topicJumpFloor") && this.state.topic?.floorInput) {
|
|
322
|
+
const floor = Number(this.state.topic.floorInput);
|
|
323
|
+
this.state.topic.jumpTarget = { type: "floor", value: floor };
|
|
324
|
+
this.state.topic.floorInput = "";
|
|
325
|
+
this.state.status = `跳转到第 ${floor} 楼?${this.keybindings.formatActionKeys("confirm")} 确认 ${this.keybindings.formatActionKeys("back")} 取消`;
|
|
275
326
|
this.render();
|
|
276
327
|
return;
|
|
277
328
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.state.
|
|
329
|
+
// 跳到最后一页
|
|
330
|
+
if (this.keybindings.matches(key, "topicJumpLast") && !this.state.topic?.floorInput && this.state.topic) {
|
|
331
|
+
const pageInfo = getTopicPageInfo(this.state.topic, this.state.topic.cursorLine);
|
|
332
|
+
this.state.topic.jumpTarget = { type: "page", value: pageInfo.totalPages };
|
|
333
|
+
this.state.status = `跳转到最后一页(第 ${pageInfo.totalPages} 页)?${this.keybindings.formatActionKeys("confirm")} 确认 ${this.keybindings.formatActionKeys("back")} 取消`;
|
|
281
334
|
this.render();
|
|
282
335
|
return;
|
|
283
336
|
}
|
|
284
|
-
|
|
337
|
+
// 确认跳转
|
|
338
|
+
if (this.keybindings.matches(key, "confirm") && this.state.topic?.jumpTarget) {
|
|
339
|
+
const target = this.state.topic.jumpTarget;
|
|
340
|
+
this.state.topic.jumpTarget = undefined;
|
|
341
|
+
this.state.status = "";
|
|
342
|
+
if (target.type === "page") {
|
|
343
|
+
void this.jumpToTopicPage(target.value, this.nextSignal());
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
void this.jumpToTopicFloor(target.value, this.nextSignal());
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// 取消跳转
|
|
351
|
+
if (this.keybindings.matches(key, "back") && (this.state.topic?.floorInput || this.state.topic?.jumpTarget)) {
|
|
285
352
|
this.state.topic.floorInput = "";
|
|
286
|
-
this.state.
|
|
353
|
+
this.state.topic.jumpTarget = undefined;
|
|
354
|
+
this.state.status = "";
|
|
287
355
|
this.render();
|
|
288
356
|
return;
|
|
289
357
|
}
|
|
290
|
-
|
|
358
|
+
// ]:下一层
|
|
359
|
+
if (this.keybindings.matches(key, "topicNextFloor") && this.state.topic) {
|
|
360
|
+
void this.jumpRelativeFloor(1);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// [:上一层
|
|
364
|
+
if (this.keybindings.matches(key, "topicPrevFloor") && this.state.topic) {
|
|
365
|
+
void this.jumpRelativeFloor(-1);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// }:下一页
|
|
369
|
+
if (this.keybindings.matches(key, "topicNextPage") && this.state.topic) {
|
|
370
|
+
void this.jumpToNextPage();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// {:上一页
|
|
374
|
+
if (this.keybindings.matches(key, "topicPrevPage") && this.state.topic) {
|
|
375
|
+
void this.jumpToPrevPage();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// h/Esc:返回
|
|
379
|
+
if (this.keybindings.matches(key, "back")) {
|
|
291
380
|
this.leave();
|
|
292
381
|
return;
|
|
293
382
|
}
|
|
294
|
-
|
|
383
|
+
// j:下移
|
|
384
|
+
if (this.keybindings.matches(key, "topicScrollDown")) {
|
|
295
385
|
const maxScroll = Math.max(0, (this.state.topic?.lines.length ?? 0) - 1);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.render();
|
|
299
|
-
if (wasAtEnd && this.state.topic?.hasMore && !this.state.loadingMore) {
|
|
300
|
-
void this.loadNextTopicPage(this.nextSignal(), true);
|
|
386
|
+
if (this.state.topic) {
|
|
387
|
+
this.state.topic.cursorLine = Math.min(maxScroll, this.state.topic.cursorLine + 1);
|
|
301
388
|
}
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
if (key === "k" || key === "\x1b[A") {
|
|
305
|
-
this.state.scroll = Math.max(0, this.state.scroll - 1);
|
|
389
|
+
this.state.status = "";
|
|
306
390
|
this.render();
|
|
391
|
+
void this.checkAutoLoad();
|
|
307
392
|
return;
|
|
308
393
|
}
|
|
309
|
-
|
|
310
|
-
|
|
394
|
+
// k:上移
|
|
395
|
+
if (this.keybindings.matches(key, "topicScrollUp")) {
|
|
396
|
+
if (this.state.topic) {
|
|
397
|
+
this.state.topic.cursorLine = Math.max(0, this.state.topic.cursorLine - 1);
|
|
398
|
+
}
|
|
399
|
+
this.state.status = "";
|
|
400
|
+
this.render();
|
|
311
401
|
return;
|
|
312
402
|
}
|
|
313
|
-
|
|
403
|
+
// r:刷新
|
|
404
|
+
if (this.keybindings.matches(key, "topicRefresh") && this.state.topic) {
|
|
314
405
|
void this.openTopic(this.state.topic.topicId, true, this.nextSignal());
|
|
315
406
|
return;
|
|
316
407
|
}
|
|
317
|
-
|
|
408
|
+
// s:收藏
|
|
409
|
+
if (this.keybindings.matches(key, "topicFavorite"))
|
|
318
410
|
void this.toggleFavorite();
|
|
319
|
-
|
|
411
|
+
// l:点赞
|
|
412
|
+
if (this.keybindings.matches(key, "topicLike"))
|
|
320
413
|
void this.reactToCurrentPost(true);
|
|
321
|
-
|
|
414
|
+
// d:踩
|
|
415
|
+
if (this.keybindings.matches(key, "topicDislike"))
|
|
322
416
|
void this.reactToCurrentPost(false);
|
|
323
|
-
|
|
417
|
+
// u:查看用户
|
|
418
|
+
if (this.keybindings.matches(key, "topicUser"))
|
|
324
419
|
void this.showCurrentUser(this.nextSignal());
|
|
325
|
-
|
|
420
|
+
// v:查看投票
|
|
421
|
+
if (this.keybindings.matches(key, "topicVote"))
|
|
326
422
|
void this.showTopicVote(this.nextSignal());
|
|
327
|
-
|
|
423
|
+
// a:查看评价
|
|
424
|
+
if (this.keybindings.matches(key, "topicReaction"))
|
|
328
425
|
void this.showPostReactionState(this.nextSignal());
|
|
329
|
-
|
|
330
|
-
|
|
426
|
+
// o:打开图片/菜单
|
|
427
|
+
if (this.keybindings.matches(key, "topicOpenImage")) {
|
|
331
428
|
const currentLine = this.getCurrentTopicLine();
|
|
332
429
|
if (currentLine?.kind === "image" && currentLine.imageUrl) {
|
|
333
430
|
void this.openImage(currentLine.imageUrl);
|
|
@@ -336,11 +433,11 @@ export class TuiController {
|
|
|
336
433
|
this.openMenu();
|
|
337
434
|
}
|
|
338
435
|
}
|
|
339
|
-
|
|
340
|
-
|
|
436
|
+
// c:图片复制图片本体,链接复制 URL
|
|
437
|
+
if (this.keybindings.matches(key, "topicCopyLink")) {
|
|
341
438
|
const currentLine = this.getCurrentTopicLine();
|
|
342
439
|
if (currentLine?.kind === "image" && currentLine.imageUrl) {
|
|
343
|
-
void this.
|
|
440
|
+
void this.copyImageToClipboard(currentLine.imageUrl);
|
|
344
441
|
}
|
|
345
442
|
else if (currentLine?.kind === "link" && currentLine.linkUrl) {
|
|
346
443
|
void this.copyToClipboard(currentLine.linkUrl);
|
|
@@ -348,39 +445,40 @@ export class TuiController {
|
|
|
348
445
|
}
|
|
349
446
|
}
|
|
350
447
|
handleSettingsKey(key) {
|
|
351
|
-
|
|
352
|
-
|
|
448
|
+
const itemCount = this.state.items.length || settingsItems.length;
|
|
449
|
+
if (this.keybindings.matches(key, "moveDown")) {
|
|
450
|
+
this.state.itemIndex = Math.min(itemCount - 1, this.state.itemIndex + 1);
|
|
353
451
|
this.render();
|
|
354
452
|
return;
|
|
355
453
|
}
|
|
356
|
-
if (key
|
|
454
|
+
if (this.keybindings.matches(key, "moveUp")) {
|
|
357
455
|
this.state.itemIndex = Math.max(0, this.state.itemIndex - 1);
|
|
358
456
|
this.render();
|
|
359
457
|
return;
|
|
360
458
|
}
|
|
361
|
-
if (key
|
|
459
|
+
if (this.keybindings.matches(key, "back")) {
|
|
362
460
|
this.state.mode = "list";
|
|
363
461
|
this.state.focus = "nav";
|
|
364
462
|
this.state.status = getStatus(this.state);
|
|
365
463
|
this.render();
|
|
366
464
|
return;
|
|
367
465
|
}
|
|
368
|
-
if (key
|
|
369
|
-
void this.activateSetting(settingsItems[this.state.itemIndex]);
|
|
466
|
+
if (this.keybindings.matches(key, "confirm") || this.keybindings.matches(key, "moveRight")) {
|
|
467
|
+
void this.activateSetting(this.state.items[this.state.itemIndex] ?? settingsItems[this.state.itemIndex]);
|
|
370
468
|
}
|
|
371
469
|
}
|
|
372
470
|
handleNavKey(key) {
|
|
373
|
-
if (key
|
|
471
|
+
if (this.keybindings.matches(key, "moveDown")) {
|
|
374
472
|
this.state.navIndex = Math.min(navItems.length - 1, this.state.navIndex + 1);
|
|
375
473
|
void this.load();
|
|
376
474
|
return;
|
|
377
475
|
}
|
|
378
|
-
if (key
|
|
476
|
+
if (this.keybindings.matches(key, "moveUp")) {
|
|
379
477
|
this.state.navIndex = Math.max(0, this.state.navIndex - 1);
|
|
380
478
|
void this.load();
|
|
381
479
|
return;
|
|
382
480
|
}
|
|
383
|
-
if (key
|
|
481
|
+
if (this.keybindings.matches(key, "confirm") || this.keybindings.matches(key, "moveRight")) {
|
|
384
482
|
if (!this.state.loading && this.state.items.length > 0) {
|
|
385
483
|
if (navItems[this.state.navIndex]?.id === "settings")
|
|
386
484
|
this.state.mode = "settings";
|
|
@@ -391,25 +489,25 @@ export class TuiController {
|
|
|
391
489
|
}
|
|
392
490
|
return;
|
|
393
491
|
}
|
|
394
|
-
if (key
|
|
492
|
+
if (this.keybindings.matches(key, "refresh"))
|
|
395
493
|
void this.load(true);
|
|
396
494
|
}
|
|
397
495
|
handleContentKey(key) {
|
|
398
|
-
if (key
|
|
496
|
+
if (this.keybindings.matches(key, "listNext")) {
|
|
399
497
|
this.state.itemIndex = Math.min(Math.max(0, this.state.items.length - 1), this.state.itemIndex + 1);
|
|
400
498
|
this.render();
|
|
401
499
|
return;
|
|
402
500
|
}
|
|
403
|
-
if (key
|
|
501
|
+
if (this.keybindings.matches(key, "listPrev")) {
|
|
404
502
|
this.state.itemIndex = Math.max(0, this.state.itemIndex - 1);
|
|
405
503
|
this.render();
|
|
406
504
|
return;
|
|
407
505
|
}
|
|
408
|
-
if (key
|
|
506
|
+
if (this.keybindings.matches(key, "listBack")) {
|
|
409
507
|
this.leave();
|
|
410
508
|
return;
|
|
411
509
|
}
|
|
412
|
-
if (key
|
|
510
|
+
if (this.keybindings.matches(key, "listOpen")) {
|
|
413
511
|
const selected = this.state.items[this.state.itemIndex];
|
|
414
512
|
if (selected) {
|
|
415
513
|
void this.activateContentItem(selected, this.nextSignal());
|
|
@@ -420,15 +518,11 @@ export class TuiController {
|
|
|
420
518
|
}
|
|
421
519
|
return;
|
|
422
520
|
}
|
|
423
|
-
if ((key
|
|
424
|
-
void this.loadNextChatPage(this.nextSignal());
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (key === "r")
|
|
521
|
+
if (this.keybindings.matches(key, "listRefresh"))
|
|
428
522
|
void this.refresh();
|
|
429
|
-
if (key
|
|
523
|
+
if (this.keybindings.matches(key, "search"))
|
|
430
524
|
this.openSearch();
|
|
431
|
-
if (key
|
|
525
|
+
if (this.keybindings.matches(key, "menu"))
|
|
432
526
|
this.openMenu();
|
|
433
527
|
}
|
|
434
528
|
leave() {
|
|
@@ -625,11 +719,18 @@ export class TuiController {
|
|
|
625
719
|
};
|
|
626
720
|
}
|
|
627
721
|
case "settings": {
|
|
628
|
-
const cacheStats = await
|
|
722
|
+
const [cacheStats, autoSignin] = await Promise.all([
|
|
723
|
+
this.client.getCacheStats(),
|
|
724
|
+
this.settingsStore.isAutoSigninEnabled()
|
|
725
|
+
]);
|
|
629
726
|
return {
|
|
630
727
|
title: "设置",
|
|
631
|
-
items:
|
|
632
|
-
stats: [
|
|
728
|
+
items: this.renderSettingsItems(autoSignin),
|
|
729
|
+
stats: [
|
|
730
|
+
{ title: "自动签到", detail: autoSignin ? "已开启" : "已关闭" },
|
|
731
|
+
{ title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` },
|
|
732
|
+
{ title: "版本", detail: `v${appVersion}` }
|
|
733
|
+
],
|
|
633
734
|
status: "设置:j/k 选择 Enter 执行 h 返回"
|
|
634
735
|
};
|
|
635
736
|
}
|
|
@@ -643,34 +744,97 @@ export class TuiController {
|
|
|
643
744
|
this.render();
|
|
644
745
|
return;
|
|
645
746
|
}
|
|
747
|
+
if (selected.meta === "keybindings") {
|
|
748
|
+
void this.openKeybindingEditor();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
646
751
|
if (selected.meta === "cache") {
|
|
647
|
-
this.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
this.render();
|
|
657
|
-
}
|
|
752
|
+
void this.openCacheManager();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (selected.meta === "pixel-logo") {
|
|
756
|
+
this.openPixelLogo();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (selected.meta === "emoji-preview") {
|
|
760
|
+
this.openEmojiPreview();
|
|
658
761
|
return;
|
|
659
762
|
}
|
|
660
763
|
if (selected.meta === "update") {
|
|
661
|
-
this.
|
|
662
|
-
|
|
764
|
+
void this.checkUpdate(true);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (selected.meta === "account") {
|
|
768
|
+
void this.openAccountSwitcher();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (selected.meta === "auto-signin") {
|
|
772
|
+
void this.toggleAutoSignin();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (selected.meta === "logout") {
|
|
776
|
+
void this.confirmLogout();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this.state.status = "功能开发中...";
|
|
780
|
+
this.render();
|
|
781
|
+
}
|
|
782
|
+
renderSettingsItems(autoSignin) {
|
|
783
|
+
return settingsItems.map((item) => {
|
|
784
|
+
if (item.meta !== "auto-signin") {
|
|
785
|
+
return { ...item };
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
...item,
|
|
789
|
+
title: `自动签到: ${autoSignin ? "开启" : "关闭"}`,
|
|
790
|
+
detail: autoSignin
|
|
791
|
+
? "启动后为所有账号执行每日签到"
|
|
792
|
+
: "默认关闭,启动时不自动签到"
|
|
793
|
+
};
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
async toggleAutoSignin() {
|
|
797
|
+
const enabled = await this.settingsStore.isAutoSigninEnabled();
|
|
798
|
+
const next = !enabled;
|
|
799
|
+
await this.settingsStore.setAutoSigninEnabled(next);
|
|
800
|
+
this.state.items = this.renderSettingsItems(next);
|
|
801
|
+
this.state.stats = [
|
|
802
|
+
{ title: "自动签到", detail: next ? "已开启" : "已关闭" },
|
|
803
|
+
...this.state.stats.filter((item) => item.title !== "自动签到")
|
|
804
|
+
];
|
|
805
|
+
this.state.status = next ? "已开启自动签到" : "已关闭自动签到";
|
|
806
|
+
this.render();
|
|
807
|
+
}
|
|
808
|
+
async runAutoSignin() {
|
|
809
|
+
const enabled = await this.settingsStore.isAutoSigninEnabled();
|
|
810
|
+
if (!enabled)
|
|
811
|
+
return;
|
|
812
|
+
const accounts = await this.tokenStore.listAccounts();
|
|
813
|
+
if (accounts.length === 0)
|
|
814
|
+
return;
|
|
815
|
+
let success = 0;
|
|
816
|
+
let failed = 0;
|
|
817
|
+
this.state.status = `自动签到: 0/${accounts.length}`;
|
|
818
|
+
this.render();
|
|
819
|
+
for (const account of accounts) {
|
|
663
820
|
try {
|
|
664
|
-
const
|
|
665
|
-
|
|
821
|
+
const tokenStore = this.tokenStore.withAccount(account.account);
|
|
822
|
+
const client = new Cc98Client({ tokenStore, webVpn: this.webVpnOptions });
|
|
823
|
+
if (this.webVpnOptions) {
|
|
824
|
+
await client.initWebVpn();
|
|
825
|
+
}
|
|
826
|
+
await client.signin();
|
|
827
|
+
success += 1;
|
|
666
828
|
}
|
|
667
|
-
catch
|
|
668
|
-
|
|
829
|
+
catch {
|
|
830
|
+
failed += 1;
|
|
669
831
|
}
|
|
832
|
+
this.state.status = `自动签到: ${success + failed}/${accounts.length}`;
|
|
670
833
|
this.render();
|
|
671
|
-
return;
|
|
672
834
|
}
|
|
673
|
-
this.state.status =
|
|
835
|
+
this.state.status = failed > 0
|
|
836
|
+
? `自动签到完成: ${success} 成功,${failed} 失败`
|
|
837
|
+
: `自动签到完成: ${success} 个账号`;
|
|
674
838
|
this.render();
|
|
675
839
|
}
|
|
676
840
|
async activateContentItem(selected, signal) {
|
|
@@ -690,6 +854,25 @@ export class TuiController {
|
|
|
690
854
|
await this.showUserDetailById(selected.userId, signal);
|
|
691
855
|
return;
|
|
692
856
|
}
|
|
857
|
+
// 账号切换
|
|
858
|
+
if (selected.meta?.startsWith("account:")) {
|
|
859
|
+
const accountName = selected.meta.slice(8);
|
|
860
|
+
await this.switchAccount(accountName);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (selected.meta?.startsWith("emoji-category:")) {
|
|
864
|
+
this.openEmojiCategory(selected.meta.slice("emoji-category:".length));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (selected.meta?.startsWith("emoji-batch:")) {
|
|
868
|
+
this.state.status = "继续向下选择具体表情,Enter 放大预览";
|
|
869
|
+
this.render();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (selected.meta?.startsWith("emoji:")) {
|
|
873
|
+
this.openEmojiDetail(selected.meta.slice("emoji:".length));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
693
876
|
if (selected.action?.startsWith("notices:")) {
|
|
694
877
|
await this.openNoticeList(selected.action.split(":")[1], signal);
|
|
695
878
|
return;
|
|
@@ -725,6 +908,10 @@ export class TuiController {
|
|
|
725
908
|
}
|
|
726
909
|
}
|
|
727
910
|
this.render();
|
|
911
|
+
// Start background image preloading so preview/open/copy can reuse the local cache.
|
|
912
|
+
if (this.state.topic) {
|
|
913
|
+
void this.preloadTopicImages(this.state.topic);
|
|
914
|
+
}
|
|
728
915
|
}
|
|
729
916
|
async openBoard(boardId, title, force, signal, pushParent = true) {
|
|
730
917
|
if (pushParent)
|
|
@@ -800,6 +987,94 @@ export class TuiController {
|
|
|
800
987
|
this.render();
|
|
801
988
|
}
|
|
802
989
|
}
|
|
990
|
+
async checkAutoLoad() {
|
|
991
|
+
const topic = this.state.topic;
|
|
992
|
+
if (!topic?.hasMore || this.state.loadingMore)
|
|
993
|
+
return;
|
|
994
|
+
const viewportRows = Math.max(1, topic.viewportRows);
|
|
995
|
+
const viewportBottom = this.state.scroll + viewportRows;
|
|
996
|
+
if (viewportBottom >= topic.lines.length) {
|
|
997
|
+
void this.loadNextTopicPage(this.nextSignal(), true);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async jumpRelativeFloor(delta) {
|
|
1001
|
+
const topic = this.state.topic;
|
|
1002
|
+
if (!topic)
|
|
1003
|
+
return;
|
|
1004
|
+
const current = currentTopicPost(topic, topic.cursorLine);
|
|
1005
|
+
const currentFloor = current?.floor ?? 1;
|
|
1006
|
+
const targetFloor = currentFloor + delta;
|
|
1007
|
+
if (targetFloor < 1)
|
|
1008
|
+
return;
|
|
1009
|
+
const loaded = findTopicPostByFloor(topic, targetFloor);
|
|
1010
|
+
if (loaded) {
|
|
1011
|
+
topic.cursorLine = loaded.lineStart;
|
|
1012
|
+
this.state.status = "";
|
|
1013
|
+
this.render();
|
|
1014
|
+
if (delta > 0)
|
|
1015
|
+
void this.checkAutoLoad();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (delta > 0 && topic.hasMore && !this.state.loadingMore) {
|
|
1019
|
+
await this.jumpToTopicFloor(targetFloor, this.nextSignal());
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async jumpToNextPage() {
|
|
1023
|
+
const topic = this.state.topic;
|
|
1024
|
+
if (!topic)
|
|
1025
|
+
return;
|
|
1026
|
+
const pageInfo = getTopicPageInfo(topic, topic.cursorLine);
|
|
1027
|
+
if (pageInfo.currentPage < pageInfo.totalPages) {
|
|
1028
|
+
await this.jumpToTopicPage(pageInfo.currentPage + 1, this.nextSignal());
|
|
1029
|
+
}
|
|
1030
|
+
else if (topic.hasMore && !this.state.loadingMore) {
|
|
1031
|
+
// 当前是最后一页,但还有更多内容,加载下一页
|
|
1032
|
+
await this.loadNextTopicPage(this.nextSignal());
|
|
1033
|
+
const newPageInfo = getTopicPageInfo(topic, topic.cursorLine);
|
|
1034
|
+
topic.cursorLine = jumpToPage(topic, newPageInfo.currentPage + 1);
|
|
1035
|
+
this.state.status = "";
|
|
1036
|
+
this.render();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async jumpToPrevPage() {
|
|
1040
|
+
const topic = this.state.topic;
|
|
1041
|
+
if (!topic)
|
|
1042
|
+
return;
|
|
1043
|
+
const pageInfo = getTopicPageInfo(topic, topic.cursorLine);
|
|
1044
|
+
if (pageInfo.currentPage > 1) {
|
|
1045
|
+
topic.cursorLine = jumpToPage(topic, pageInfo.currentPage - 1);
|
|
1046
|
+
this.state.status = "";
|
|
1047
|
+
this.render();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async jumpToTopicPage(page, signal) {
|
|
1051
|
+
const topic = this.state.topic;
|
|
1052
|
+
if (!topic)
|
|
1053
|
+
return;
|
|
1054
|
+
const pageInfo = getTopicPageInfo(topic, topic.cursorLine);
|
|
1055
|
+
if (page < 1 || page > pageInfo.totalPages) {
|
|
1056
|
+
this.state.status = `未找到第 ${page} 页`;
|
|
1057
|
+
this.render();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
// 如果目标页未加载,需要加载到该页
|
|
1061
|
+
const targetFloor = (page - 1) * FLOORS_PER_PAGE + 1;
|
|
1062
|
+
while (topic.hasMore && !findTopicPostByFloor(topic, targetFloor)) {
|
|
1063
|
+
const previousLoaded = topic.loaded;
|
|
1064
|
+
await this.loadNextTopicPage(signal, true);
|
|
1065
|
+
if (topic.loaded === previousLoaded)
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
const post = findTopicPostByFloor(topic, targetFloor);
|
|
1069
|
+
if (post) {
|
|
1070
|
+
topic.cursorLine = post.lineStart;
|
|
1071
|
+
this.state.status = "";
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
this.state.status = `未找到第 ${page} 页`;
|
|
1075
|
+
}
|
|
1076
|
+
this.render();
|
|
1077
|
+
}
|
|
803
1078
|
async loadNextTopicPage(signal, quiet = false) {
|
|
804
1079
|
const topic = this.state.topic;
|
|
805
1080
|
if (!topic?.hasMore || this.state.loadingMore)
|
|
@@ -810,7 +1085,8 @@ export class TuiController {
|
|
|
810
1085
|
try {
|
|
811
1086
|
const posts = asArray(await this.client.getTopicPosts(topic.topicId, topic.loaded, topic.size, false, signal));
|
|
812
1087
|
appendTopicPosts(topic, posts);
|
|
813
|
-
this.state.status =
|
|
1088
|
+
this.state.status = "";
|
|
1089
|
+
void this.preloadTopicImages(topic);
|
|
814
1090
|
}
|
|
815
1091
|
catch (error) {
|
|
816
1092
|
if (!isAbortError(error))
|
|
@@ -827,17 +1103,20 @@ export class TuiController {
|
|
|
827
1103
|
return;
|
|
828
1104
|
const loaded = findTopicPostByFloor(topic, floor);
|
|
829
1105
|
if (loaded) {
|
|
830
|
-
|
|
831
|
-
this.state.status =
|
|
1106
|
+
topic.cursorLine = loaded.lineStart;
|
|
1107
|
+
this.state.status = "";
|
|
832
1108
|
this.render();
|
|
833
1109
|
return;
|
|
834
1110
|
}
|
|
835
1111
|
while (topic.hasMore && !findTopicPostByFloor(topic, floor)) {
|
|
1112
|
+
const previousLoaded = topic.loaded;
|
|
836
1113
|
await this.loadNextTopicPage(signal, true);
|
|
1114
|
+
if (topic.loaded === previousLoaded)
|
|
1115
|
+
break;
|
|
837
1116
|
}
|
|
838
1117
|
const post = findTopicPostByFloor(topic, floor);
|
|
839
|
-
|
|
840
|
-
this.state.status = post ?
|
|
1118
|
+
topic.cursorLine = post?.lineStart ?? topic.cursorLine;
|
|
1119
|
+
this.state.status = post ? "" : `未找到 ${floor} 楼`;
|
|
841
1120
|
this.render();
|
|
842
1121
|
}
|
|
843
1122
|
async runReadOnlyAction(action, signal) {
|
|
@@ -955,7 +1234,7 @@ export class TuiController {
|
|
|
955
1234
|
const topic = this.state.topic;
|
|
956
1235
|
if (!topic)
|
|
957
1236
|
return;
|
|
958
|
-
const post = currentTopicPost(topic,
|
|
1237
|
+
const post = currentTopicPost(topic, topic.cursorLine);
|
|
959
1238
|
if (!post?.id) {
|
|
960
1239
|
this.state.status = "当前楼层没有可操作的帖子 ID";
|
|
961
1240
|
this.render();
|
|
@@ -974,7 +1253,7 @@ export class TuiController {
|
|
|
974
1253
|
const topic = this.state.topic;
|
|
975
1254
|
if (!topic)
|
|
976
1255
|
return;
|
|
977
|
-
const post = currentTopicPost(topic,
|
|
1256
|
+
const post = currentTopicPost(topic, topic.cursorLine);
|
|
978
1257
|
if (!post?.userId) {
|
|
979
1258
|
this.state.status = "当前楼层没有用户 ID";
|
|
980
1259
|
this.render();
|
|
@@ -1030,7 +1309,7 @@ export class TuiController {
|
|
|
1030
1309
|
const topic = this.state.topic;
|
|
1031
1310
|
if (!topic)
|
|
1032
1311
|
return;
|
|
1033
|
-
const post = currentTopicPost(topic,
|
|
1312
|
+
const post = currentTopicPost(topic, topic.cursorLine);
|
|
1034
1313
|
if (!post?.id)
|
|
1035
1314
|
return;
|
|
1036
1315
|
try {
|
|
@@ -1081,12 +1360,60 @@ export class TuiController {
|
|
|
1081
1360
|
if (!topic)
|
|
1082
1361
|
return undefined;
|
|
1083
1362
|
for (const post of topic.posts) {
|
|
1084
|
-
const line = post.lines.find((entry) => entry.line ===
|
|
1363
|
+
const line = post.lines.find((entry) => entry.line === topic.cursorLine);
|
|
1085
1364
|
if (line)
|
|
1086
1365
|
return line;
|
|
1087
1366
|
}
|
|
1088
1367
|
return undefined;
|
|
1089
1368
|
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Preload images for topic in background
|
|
1371
|
+
* Images are cached and trigger re-render when ready
|
|
1372
|
+
*/
|
|
1373
|
+
async preloadTopicImages(topic) {
|
|
1374
|
+
const cache = getImageCache();
|
|
1375
|
+
const imagesToLoad = new Set();
|
|
1376
|
+
// Collect all unique image URLs from posts
|
|
1377
|
+
for (const post of topic.posts) {
|
|
1378
|
+
for (const imageUrl of post.images) {
|
|
1379
|
+
if (imageUrl && !topic.imageCache.has(imageUrl) && !topic.imageLoading.has(imageUrl)) {
|
|
1380
|
+
imagesToLoad.add(imageUrl);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
if (imagesToLoad.size === 0)
|
|
1385
|
+
return;
|
|
1386
|
+
// Load images in parallel with concurrency limit
|
|
1387
|
+
const urls = Array.from(imagesToLoad);
|
|
1388
|
+
const concurrency = 3;
|
|
1389
|
+
for (let i = 0; i < urls.length; i += concurrency) {
|
|
1390
|
+
// Check if topic is still the same (user might have navigated away)
|
|
1391
|
+
if (this.state.topic !== topic)
|
|
1392
|
+
break;
|
|
1393
|
+
const batch = urls.slice(i, i + concurrency);
|
|
1394
|
+
let shouldRender = false;
|
|
1395
|
+
const promises = batch.map(async (url) => {
|
|
1396
|
+
topic.imageLoading.add(url);
|
|
1397
|
+
try {
|
|
1398
|
+
const localPath = await cache.getOrDownload(url);
|
|
1399
|
+
topic.imageErrors.delete(url);
|
|
1400
|
+
topic.imageCache.set(url, localPath);
|
|
1401
|
+
shouldRender = true;
|
|
1402
|
+
}
|
|
1403
|
+
catch (error) {
|
|
1404
|
+
topic.imageErrors.set(url, error instanceof Error ? error.message : "下载失败");
|
|
1405
|
+
shouldRender = true;
|
|
1406
|
+
}
|
|
1407
|
+
finally {
|
|
1408
|
+
topic.imageLoading.delete(url);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
await Promise.all(promises);
|
|
1412
|
+
if (shouldRender && this.state.topic === topic) {
|
|
1413
|
+
this.render();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1090
1417
|
async openImage(url) {
|
|
1091
1418
|
this.state.status = "正在下载图片...";
|
|
1092
1419
|
this.render();
|
|
@@ -1099,7 +1426,11 @@ export class TuiController {
|
|
|
1099
1426
|
const { execFile } = await import("node:child_process");
|
|
1100
1427
|
const platform = process.platform;
|
|
1101
1428
|
const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
1102
|
-
const args = platform === "win32"
|
|
1429
|
+
const args = platform === "win32"
|
|
1430
|
+
? ["/c", "start", "", localPath]
|
|
1431
|
+
: platform === "darwin"
|
|
1432
|
+
? ["-a", "Preview", localPath]
|
|
1433
|
+
: [localPath];
|
|
1103
1434
|
execFile(command, args, (error) => {
|
|
1104
1435
|
if (error) {
|
|
1105
1436
|
this.state.status = `打开失败: ${error.message}`;
|
|
@@ -1112,6 +1443,58 @@ export class TuiController {
|
|
|
1112
1443
|
this.render();
|
|
1113
1444
|
}
|
|
1114
1445
|
}
|
|
1446
|
+
async copyImageToClipboard(url) {
|
|
1447
|
+
this.state.status = "正在复制图片...";
|
|
1448
|
+
this.render();
|
|
1449
|
+
try {
|
|
1450
|
+
const cache = getImageCache();
|
|
1451
|
+
const localPath = await cache.getOrDownload(url);
|
|
1452
|
+
await this.copyImageFileToClipboard(localPath);
|
|
1453
|
+
this.state.status = "已复制图片到剪贴板";
|
|
1454
|
+
this.render();
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
this.state.status = error instanceof Error ? error.message : "复制图片失败";
|
|
1458
|
+
this.render();
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async copyImageFileToClipboard(localPath) {
|
|
1462
|
+
const platform = process.platform;
|
|
1463
|
+
if (platform === "darwin") {
|
|
1464
|
+
await this.copyImageFileToClipboardMac(localPath);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (platform === "win32") {
|
|
1468
|
+
const script = [
|
|
1469
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
1470
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
1471
|
+
"$image=[System.Drawing.Image]::FromFile($args[0])",
|
|
1472
|
+
"[System.Windows.Forms.Clipboard]::SetImage($image)",
|
|
1473
|
+
"$image.Dispose()"
|
|
1474
|
+
].join("; ");
|
|
1475
|
+
await execFilePromise("powershell.exe", ["-NoProfile", "-Command", script, localPath]);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const mime = imageMimeType(localPath);
|
|
1479
|
+
await execFilePromise("xclip", ["-selection", "clipboard", "-t", mime, localPath]);
|
|
1480
|
+
}
|
|
1481
|
+
async copyImageFileToClipboardMac(localPath) {
|
|
1482
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
1483
|
+
const { tmpdir } = await import("node:os");
|
|
1484
|
+
const { join } = await import("node:path");
|
|
1485
|
+
const dir = await mkdtemp(join(tmpdir(), "cc98-image-"));
|
|
1486
|
+
const tiffPath = join(dir, "clipboard.tiff");
|
|
1487
|
+
try {
|
|
1488
|
+
await execFilePromise("sips", ["-s", "format", "tiff", localPath, "--out", tiffPath]);
|
|
1489
|
+
await execFilePromise("osascript", [
|
|
1490
|
+
"-e",
|
|
1491
|
+
`set the clipboard to (read (POSIX file ${appleScriptString(tiffPath)}) as TIFF picture)`
|
|
1492
|
+
]);
|
|
1493
|
+
}
|
|
1494
|
+
finally {
|
|
1495
|
+
await rm(dir, { recursive: true, force: true });
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1115
1498
|
async copyToClipboard(text) {
|
|
1116
1499
|
try {
|
|
1117
1500
|
const { spawn } = await import("node:child_process");
|
|
@@ -1148,6 +1531,222 @@ export class TuiController {
|
|
|
1148
1531
|
}
|
|
1149
1532
|
this.render();
|
|
1150
1533
|
}
|
|
1534
|
+
async openKeybindingEditor() {
|
|
1535
|
+
// 显示快捷键配置信息
|
|
1536
|
+
const config = this.keybindings.getConfig();
|
|
1537
|
+
const lines = [
|
|
1538
|
+
"快捷键配置文件: ~/.cc98-cli/keybindings.json",
|
|
1539
|
+
"",
|
|
1540
|
+
"当前配置:"
|
|
1541
|
+
];
|
|
1542
|
+
// 显示主要快捷键
|
|
1543
|
+
const mainActions = [
|
|
1544
|
+
"moveUp", "moveDown", "moveLeft", "moveRight", "confirm", "back",
|
|
1545
|
+
"search", "refresh", "menu", "help", "quit",
|
|
1546
|
+
"topicNextPage", "topicPrevPage", "topicNextFloor", "topicPrevFloor",
|
|
1547
|
+
"topicJumpPage", "topicJumpFloor", "topicJumpLast"
|
|
1548
|
+
];
|
|
1549
|
+
for (const action of mainActions) {
|
|
1550
|
+
const keys = config[action] ?? [];
|
|
1551
|
+
const desc = this.keybindings.getActionDescription(action);
|
|
1552
|
+
const keyStr = keys.map(k => this.keybindings.formatKey(k)).join("/");
|
|
1553
|
+
lines.push(` ${desc}: ${keyStr}`);
|
|
1554
|
+
}
|
|
1555
|
+
lines.push("", "编辑配置文件后重启生效。", "", "按 Esc 返回设置");
|
|
1556
|
+
this.state.modal = "info";
|
|
1557
|
+
this.state.infoTitle = "快捷键设置";
|
|
1558
|
+
this.state.infoLines = lines;
|
|
1559
|
+
this.render();
|
|
1560
|
+
}
|
|
1561
|
+
openPixelLogo() {
|
|
1562
|
+
this.state.modal = "info";
|
|
1563
|
+
this.state.infoTitle = "CC98 像素 Logo";
|
|
1564
|
+
this.state.infoLines = [
|
|
1565
|
+
...renderCc98Logo().split("\n"),
|
|
1566
|
+
"",
|
|
1567
|
+
"来源: https://www.cc98.org/static/images/98LOGO.ico",
|
|
1568
|
+
"渲染: 24-bit ANSI 半块像素"
|
|
1569
|
+
];
|
|
1570
|
+
this.render();
|
|
1571
|
+
}
|
|
1572
|
+
openEmojiPreview() {
|
|
1573
|
+
const items = EMOJI_CATEGORIES.map((category) => ({
|
|
1574
|
+
title: `${category.label} (${category.codes.length})`,
|
|
1575
|
+
meta: `emoji-category:${category.id}`,
|
|
1576
|
+
detail: `来源目录: Assets/Emoji/${category.source} · ${category.codes[0]} - ${category.codes.at(-1)}`
|
|
1577
|
+
}));
|
|
1578
|
+
this.openReadOnlyList("表情包预览", items, EMOJI_CATEGORIES.map((category) => ({
|
|
1579
|
+
title: category.label,
|
|
1580
|
+
detail: `${category.codes.length} 个`
|
|
1581
|
+
})));
|
|
1582
|
+
this.state.status = "表情包预览:j/k 选择分类 Enter 进入 h 返回";
|
|
1583
|
+
this.render();
|
|
1584
|
+
}
|
|
1585
|
+
openEmojiCategory(categoryId) {
|
|
1586
|
+
const category = EMOJI_CATEGORIES.find((item) => item.id === categoryId);
|
|
1587
|
+
if (!category) {
|
|
1588
|
+
this.state.status = "未找到表情分类";
|
|
1589
|
+
this.render();
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const items = [];
|
|
1593
|
+
for (let index = 0; index < category.codes.length; index += 20) {
|
|
1594
|
+
const batch = category.codes.slice(index, index + 20);
|
|
1595
|
+
const batchNumber = Math.floor(index / 20) + 1;
|
|
1596
|
+
items.push({
|
|
1597
|
+
title: `${category.label} 第 ${batchNumber} 批`,
|
|
1598
|
+
meta: `emoji-batch:${category.id}:${batchNumber}`,
|
|
1599
|
+
detail: `${batch[0]} - ${batch.at(-1)} · ${batch.length} 个`
|
|
1600
|
+
});
|
|
1601
|
+
for (const code of batch) {
|
|
1602
|
+
const art = getEmojiArt(code);
|
|
1603
|
+
items.push({
|
|
1604
|
+
title: `[${code}]`,
|
|
1605
|
+
meta: `emoji:${code}`,
|
|
1606
|
+
detail: art ? `${category.label} · ${art.width}x${art.height}px` : category.label
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
this.openReadOnlyList(category.label, items, [
|
|
1611
|
+
{ title: "分类", detail: category.label },
|
|
1612
|
+
{ title: "数量", detail: `${category.codes.length} 个` },
|
|
1613
|
+
{ title: "来源", detail: `Assets/Emoji/${category.source}` }
|
|
1614
|
+
]);
|
|
1615
|
+
this.state.status = `${category.label}:j/k 选择 Enter 放大 h 返回分类`;
|
|
1616
|
+
this.render();
|
|
1617
|
+
}
|
|
1618
|
+
openEmojiDetail(code) {
|
|
1619
|
+
const art = getEmojiArt(code);
|
|
1620
|
+
const rendered = renderEmojiCode(code);
|
|
1621
|
+
if (!art || !rendered) {
|
|
1622
|
+
this.state.status = `未找到表情 [${code}]`;
|
|
1623
|
+
this.render();
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
this.state.modal = "info";
|
|
1627
|
+
this.state.infoTitle = `[${code}]`;
|
|
1628
|
+
this.state.infoLines = [
|
|
1629
|
+
...rendered.split("\n"),
|
|
1630
|
+
"",
|
|
1631
|
+
`尺寸: ${art.width}x${art.height}px`,
|
|
1632
|
+
`颜色: ${art.palette.length}`
|
|
1633
|
+
];
|
|
1634
|
+
this.render();
|
|
1635
|
+
}
|
|
1636
|
+
async openAccountSwitcher() {
|
|
1637
|
+
try {
|
|
1638
|
+
const accounts = await this.tokenStore.listAccounts();
|
|
1639
|
+
const currentAccount = await this.tokenStore.getCurrentAccountName();
|
|
1640
|
+
if (accounts.length === 0) {
|
|
1641
|
+
this.state.modal = "info";
|
|
1642
|
+
this.state.infoTitle = "切换账号";
|
|
1643
|
+
this.state.infoLines = ["暂无保存的账号", "", "请先登录账号。"];
|
|
1644
|
+
this.render();
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
// 构建账号列表
|
|
1648
|
+
const items = accounts.map(account => ({
|
|
1649
|
+
title: account.displayName || account.username || account.account,
|
|
1650
|
+
meta: `account:${account.account}`,
|
|
1651
|
+
detail: `${account.account === currentAccount ? "✓ 当前" : "切换到此账号"}${account.userId ? ` · ID: ${account.userId}` : ""}`
|
|
1652
|
+
}));
|
|
1653
|
+
this.snapshotParent();
|
|
1654
|
+
this.state.viewTitle = "切换账号";
|
|
1655
|
+
this.state.items = items;
|
|
1656
|
+
this.state.stats = [{ title: "账号数", detail: `${accounts.length}` }];
|
|
1657
|
+
this.state.itemIndex = accounts.findIndex(a => a.account === currentAccount);
|
|
1658
|
+
this.state.scroll = 0;
|
|
1659
|
+
this.state.focus = "content";
|
|
1660
|
+
this.state.mode = "list";
|
|
1661
|
+
this.state.status = "选择账号: j/k 选择 Enter 切换 h 返回";
|
|
1662
|
+
this.render();
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
this.state.status = error instanceof Error ? error.message : "读取账号失败";
|
|
1666
|
+
this.render();
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async switchAccount(accountName) {
|
|
1670
|
+
try {
|
|
1671
|
+
await this.tokenStore.useAccount(accountName);
|
|
1672
|
+
this.state.status = `已切换到账号: ${accountName}`;
|
|
1673
|
+
this.state.parentList = undefined;
|
|
1674
|
+
this.state.mode = "list";
|
|
1675
|
+
this.state.focus = "nav";
|
|
1676
|
+
await this.load(true);
|
|
1677
|
+
}
|
|
1678
|
+
catch (error) {
|
|
1679
|
+
this.state.status = error instanceof Error ? error.message : "切换账号失败";
|
|
1680
|
+
this.render();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async confirmLogout() {
|
|
1684
|
+
const account = await this.tokenStore.getCurrentAccountName();
|
|
1685
|
+
const lines = [
|
|
1686
|
+
`当前账号: ${account || "未知"}`,
|
|
1687
|
+
"",
|
|
1688
|
+
"退出登录将清除所有保存的账号信息。",
|
|
1689
|
+
"清除后需要重新登录。",
|
|
1690
|
+
"",
|
|
1691
|
+
`${this.keybindings.formatActionKeys("confirm")} 确认 ${this.keybindings.formatActionKeys("back")} 取消`
|
|
1692
|
+
];
|
|
1693
|
+
this.state.modal = "info";
|
|
1694
|
+
this.state.infoTitle = "退出登录";
|
|
1695
|
+
this.state.infoLines = lines;
|
|
1696
|
+
this.state.confirmCallback = () => void this.performLogout();
|
|
1697
|
+
this.render();
|
|
1698
|
+
}
|
|
1699
|
+
async performLogout() {
|
|
1700
|
+
try {
|
|
1701
|
+
await this.tokenStore.clear();
|
|
1702
|
+
this.state.status = "已退出登录";
|
|
1703
|
+
this.state.parentList = undefined;
|
|
1704
|
+
this.state.mode = "list";
|
|
1705
|
+
this.state.focus = "nav";
|
|
1706
|
+
await this.load(true);
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
this.state.status = error instanceof Error ? error.message : "退出失败";
|
|
1710
|
+
this.render();
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
async openCacheManager() {
|
|
1714
|
+
try {
|
|
1715
|
+
const stats = await this.client.getCacheStats();
|
|
1716
|
+
const cacheDir = "~/.cc98-cli/cache/";
|
|
1717
|
+
const lines = [
|
|
1718
|
+
`缓存目录: ${cacheDir}`,
|
|
1719
|
+
`文件数量: ${stats.fileCacheEntries}`,
|
|
1720
|
+
"",
|
|
1721
|
+
"缓存策略:",
|
|
1722
|
+
" 版面主题: 30s",
|
|
1723
|
+
" 版面信息: 24h",
|
|
1724
|
+
" 用户信息: 5min",
|
|
1725
|
+
"",
|
|
1726
|
+
"Enter 清理缓存 Esc 返回"
|
|
1727
|
+
];
|
|
1728
|
+
this.state.modal = "info";
|
|
1729
|
+
this.state.infoTitle = "缓存管理";
|
|
1730
|
+
this.state.infoLines = lines;
|
|
1731
|
+
this.state.confirmCallback = () => void this.clearCache();
|
|
1732
|
+
this.render();
|
|
1733
|
+
}
|
|
1734
|
+
catch (error) {
|
|
1735
|
+
this.state.status = error instanceof Error ? error.message : "读取缓存信息失败";
|
|
1736
|
+
this.render();
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
async clearCache() {
|
|
1740
|
+
try {
|
|
1741
|
+
await this.client.clearCache();
|
|
1742
|
+
this.state.status = "缓存已清理";
|
|
1743
|
+
await this.load(true);
|
|
1744
|
+
}
|
|
1745
|
+
catch {
|
|
1746
|
+
this.state.status = "缓存清理失败";
|
|
1747
|
+
this.render();
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1151
1750
|
async openFriendUsers(type, signal) {
|
|
1152
1751
|
const ids = asArray(await this.client.getFriendIds(type, 0, 20, false, signal)).filter((id) => typeof id === "number");
|
|
1153
1752
|
const users = asArray(await this.client.getUsers(ids, false, signal));
|
|
@@ -1169,15 +1768,15 @@ export class TuiController {
|
|
|
1169
1768
|
this.render();
|
|
1170
1769
|
}
|
|
1171
1770
|
snapshotParent() {
|
|
1172
|
-
|
|
1173
|
-
this.state.
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1771
|
+
this.state.parentList = {
|
|
1772
|
+
title: this.state.viewTitle,
|
|
1773
|
+
items: [...this.state.items],
|
|
1774
|
+
stats: [...this.state.stats],
|
|
1775
|
+
itemIndex: this.state.itemIndex,
|
|
1776
|
+
scroll: this.state.scroll,
|
|
1777
|
+
status: this.state.status,
|
|
1778
|
+
parent: this.state.parentList
|
|
1779
|
+
};
|
|
1181
1780
|
}
|
|
1182
1781
|
restoreParentList() {
|
|
1183
1782
|
const parent = this.state.parentList;
|
|
@@ -1187,14 +1786,93 @@ export class TuiController {
|
|
|
1187
1786
|
this.state.items = parent.items;
|
|
1188
1787
|
this.state.stats = parent.stats;
|
|
1189
1788
|
this.state.itemIndex = parent.itemIndex;
|
|
1789
|
+
this.state.scroll = parent.scroll;
|
|
1190
1790
|
this.state.status = parent.status;
|
|
1191
|
-
this.state.parentList =
|
|
1791
|
+
this.state.parentList = parent.parent;
|
|
1192
1792
|
this.state.currentBoard = undefined;
|
|
1193
1793
|
this.state.currentChat = undefined;
|
|
1194
1794
|
this.state.topic = undefined;
|
|
1195
1795
|
this.state.mode = "list";
|
|
1196
1796
|
this.state.focus = "content";
|
|
1197
|
-
this.
|
|
1797
|
+
this.render();
|
|
1798
|
+
}
|
|
1799
|
+
async checkUpdate(forceShow = false) {
|
|
1800
|
+
if (forceShow) {
|
|
1801
|
+
this.state.status = "正在检查 GitHub Release...";
|
|
1802
|
+
this.render();
|
|
1803
|
+
}
|
|
1804
|
+
try {
|
|
1805
|
+
const result = await checkForUpdate();
|
|
1806
|
+
if (!result.updateAvailable || !result.latest) {
|
|
1807
|
+
this.state.updateAvailable = undefined;
|
|
1808
|
+
if (forceShow) {
|
|
1809
|
+
this.state.status = result.message;
|
|
1810
|
+
this.render();
|
|
1811
|
+
}
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const lastSeen = await this.settingsStore.getLastSeenVersion();
|
|
1815
|
+
const isNew = forceShow || lastSeen !== result.latest.version;
|
|
1816
|
+
this.state.updateAvailable = {
|
|
1817
|
+
version: result.latest.version,
|
|
1818
|
+
tagName: result.latest.tagName,
|
|
1819
|
+
url: result.latest.url,
|
|
1820
|
+
body: result.latest.body,
|
|
1821
|
+
isNew
|
|
1822
|
+
};
|
|
1823
|
+
if (forceShow) {
|
|
1824
|
+
this.state.status = result.message;
|
|
1825
|
+
}
|
|
1826
|
+
this.render();
|
|
1827
|
+
}
|
|
1828
|
+
catch (error) {
|
|
1829
|
+
if (forceShow) {
|
|
1830
|
+
this.state.status = error instanceof Error ? error.message : "检查更新失败";
|
|
1831
|
+
this.render();
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1198
1834
|
}
|
|
1835
|
+
dismissUpdate() {
|
|
1836
|
+
if (this.state.updateAvailable) {
|
|
1837
|
+
const version = this.state.updateAvailable.version;
|
|
1838
|
+
this.state.updateAvailable = undefined;
|
|
1839
|
+
this.render();
|
|
1840
|
+
void this.settingsStore.setLastSeenVersion(version).catch(() => {
|
|
1841
|
+
// 忽略已读状态写入失败,避免影响 TUI 操作。
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
async function execFilePromise(command, args) {
|
|
1847
|
+
const { execFile } = await import("node:child_process");
|
|
1848
|
+
await new Promise((resolve, reject) => {
|
|
1849
|
+
execFile(command, args, (error) => {
|
|
1850
|
+
if (error) {
|
|
1851
|
+
reject(error);
|
|
1852
|
+
}
|
|
1853
|
+
else {
|
|
1854
|
+
resolve();
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
function appleScriptString(value) {
|
|
1860
|
+
return JSON.stringify(value);
|
|
1861
|
+
}
|
|
1862
|
+
function imageMimeType(path) {
|
|
1863
|
+
const lower = path.toLowerCase();
|
|
1864
|
+
if (lower.endsWith(".png"))
|
|
1865
|
+
return "image/png";
|
|
1866
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
1867
|
+
return "image/jpeg";
|
|
1868
|
+
if (lower.endsWith(".gif"))
|
|
1869
|
+
return "image/gif";
|
|
1870
|
+
if (lower.endsWith(".webp"))
|
|
1871
|
+
return "image/webp";
|
|
1872
|
+
if (lower.endsWith(".bmp"))
|
|
1873
|
+
return "image/bmp";
|
|
1874
|
+
if (lower.endsWith(".svg"))
|
|
1875
|
+
return "image/svg+xml";
|
|
1876
|
+
return "image/png";
|
|
1199
1877
|
}
|
|
1200
1878
|
//# sourceMappingURL=controller.js.map
|