cc98-cli 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +4 -1
- package/dist/api/client.d.ts +1 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +3 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/endpoints.d.ts +1 -0
- package/dist/api/endpoints.d.ts.map +1 -1
- package/dist/api/endpoints.js +1 -0
- package/dist/api/endpoints.js.map +1 -1
- package/dist/cli/commands/topic.d.ts.map +1 -1
- package/dist/cli/commands/topic.js +7 -0
- package/dist/cli/commands/topic.js.map +1 -1
- package/dist/cli/router.js +3 -0
- package/dist/cli/router.js.map +1 -1
- package/dist/storage/settings-store.d.ts +2 -0
- package/dist/storage/settings-store.d.ts.map +1 -1
- package/dist/storage/settings-store.js +9 -0
- package/dist/storage/settings-store.js.map +1 -1
- 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/cached-client.d.ts +1 -0
- package/dist/tui/cached-client.d.ts.map +1 -1
- package/dist/tui/cached-client.js +3 -0
- package/dist/tui/cached-client.js.map +1 -1
- package/dist/tui/components/content.d.ts +1 -0
- package/dist/tui/components/content.d.ts.map +1 -1
- package/dist/tui/components/content.js +26 -7
- package/dist/tui/components/content.js.map +1 -1
- package/dist/tui/components/sidebar.d.ts.map +1 -1
- package/dist/tui/components/sidebar.js +1 -11
- package/dist/tui/components/sidebar.js.map +1 -1
- package/dist/tui/components/status.d.ts.map +1 -1
- package/dist/tui/components/status.js +3 -0
- 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 +38 -1
- package/dist/tui/controller.d.ts.map +1 -1
- package/dist/tui/controller.js +731 -71
- 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.js +1 -1
- package/dist/tui/keybindings.js.map +1 -1
- package/dist/tui/navigation.d.ts.map +1 -1
- package/dist/tui/navigation.js +5 -3
- package/dist/tui/navigation.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +132 -5
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/state/store.d.ts.map +1 -1
- package/dist/tui/state/store.js +2 -1
- package/dist/tui/state/store.js.map +1 -1
- package/dist/tui/state/types.d.ts +35 -2
- 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 +2 -1
- package/dist/tui/topic-reader.d.ts.map +1 -1
- package/dist/tui/topic-reader.js +25 -9
- 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/package.json +2 -1
package/dist/tui/controller.js
CHANGED
|
@@ -1,12 +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";
|
|
4
5
|
import { SettingsStore } from "../storage/settings-store.js";
|
|
5
6
|
import { getKeybindingManager } from "./keybindings.js";
|
|
7
|
+
import { EMOJI_CATEGORIES, getEmojiArt, renderCc98Logo, renderEmojiCode } from "./emoji-renderer.js";
|
|
6
8
|
import { navItems, settingsItems } from "./navigation.js";
|
|
7
9
|
import { getStatus } from "./state/store.js";
|
|
8
10
|
import { asArray, asNumber, asObject, chatItem, chatMessageItems, flattenBoards, genericItem, historyItem, isAbortError, jsonPreviewLines, loadChatUserNames, mapLimit, noticeItem, overviewStats, topicItem, unreadStats, userItem } from "./helpers.js";
|
|
9
|
-
import { appendTopicPosts, buildTopicReader, currentTopicPost, findTopicPostByFloor, getTopicPageInfo, jumpToPage, FLOORS_PER_PAGE } from "./topic-reader.js";
|
|
11
|
+
import { appendTopicPosts, buildTopicReader, currentTopicLine, currentTopicPost, findTopicPostByFloor, getTopicPageInfo, jumpToPage, replaceTopicPosts, FLOORS_PER_PAGE } from "./topic-reader.js";
|
|
10
12
|
export class TuiController {
|
|
11
13
|
state;
|
|
12
14
|
client;
|
|
@@ -15,11 +17,14 @@ export class TuiController {
|
|
|
15
17
|
close;
|
|
16
18
|
nextSignal;
|
|
17
19
|
abortCurrent;
|
|
20
|
+
webVpnOptions;
|
|
18
21
|
loadVersion = 0;
|
|
19
22
|
keybindings;
|
|
20
23
|
settingsStore = new SettingsStore();
|
|
21
24
|
updateChecked = false;
|
|
22
|
-
|
|
25
|
+
autoSigninChecked = false;
|
|
26
|
+
listReturnState;
|
|
27
|
+
constructor(state, client, tokenStore, render, close, nextSignal, abortCurrent, webVpnOptions) {
|
|
23
28
|
this.state = state;
|
|
24
29
|
this.client = client;
|
|
25
30
|
this.tokenStore = tokenStore;
|
|
@@ -27,10 +32,12 @@ export class TuiController {
|
|
|
27
32
|
this.close = close;
|
|
28
33
|
this.nextSignal = nextSignal;
|
|
29
34
|
this.abortCurrent = abortCurrent;
|
|
35
|
+
this.webVpnOptions = webVpnOptions;
|
|
30
36
|
this.keybindings = getKeybindingManager();
|
|
31
37
|
}
|
|
32
38
|
async load(force = false) {
|
|
33
39
|
const version = ++this.loadVersion;
|
|
40
|
+
let shouldAutoSignin = false;
|
|
34
41
|
const signal = this.nextSignal();
|
|
35
42
|
const nav = navItems[this.state.navIndex] ?? navItems[0];
|
|
36
43
|
this.state.viewTitle = nav.label;
|
|
@@ -48,6 +55,7 @@ export class TuiController {
|
|
|
48
55
|
this.state.parentList = undefined;
|
|
49
56
|
this.state.currentBoard = undefined;
|
|
50
57
|
this.state.currentChat = undefined;
|
|
58
|
+
this.state.listPaging = undefined;
|
|
51
59
|
this.render();
|
|
52
60
|
try {
|
|
53
61
|
// 加载快捷键配置
|
|
@@ -58,12 +66,17 @@ export class TuiController {
|
|
|
58
66
|
this.updateChecked = true;
|
|
59
67
|
void this.checkUpdate();
|
|
60
68
|
}
|
|
69
|
+
if (!this.autoSigninChecked) {
|
|
70
|
+
this.autoSigninChecked = true;
|
|
71
|
+
shouldAutoSignin = true;
|
|
72
|
+
}
|
|
61
73
|
const next = await this.loadView(nav.id, force, signal);
|
|
62
74
|
if (version !== this.loadVersion)
|
|
63
75
|
return;
|
|
64
76
|
this.state.viewTitle = next.title;
|
|
65
77
|
this.state.items = next.items;
|
|
66
78
|
this.state.stats = next.stats;
|
|
79
|
+
this.state.listPaging = next.paging;
|
|
67
80
|
if (next.overview) {
|
|
68
81
|
this.state.overview = next.overview;
|
|
69
82
|
}
|
|
@@ -80,6 +93,12 @@ export class TuiController {
|
|
|
80
93
|
if (version === this.loadVersion) {
|
|
81
94
|
this.state.loading = false;
|
|
82
95
|
this.render();
|
|
96
|
+
if (this.state.listPaging?.hasMore) {
|
|
97
|
+
void this.ensureListWindowFilled(this.nextSignal());
|
|
98
|
+
}
|
|
99
|
+
if (shouldAutoSignin) {
|
|
100
|
+
void this.runAutoSignin();
|
|
101
|
+
}
|
|
83
102
|
}
|
|
84
103
|
}
|
|
85
104
|
}
|
|
@@ -176,9 +195,7 @@ export class TuiController {
|
|
|
176
195
|
}
|
|
177
196
|
handleSearchKey(key) {
|
|
178
197
|
if (this.keybindings.matches(key, "searchClose")) {
|
|
179
|
-
this.
|
|
180
|
-
this.state.searchQuery = "";
|
|
181
|
-
this.render();
|
|
198
|
+
this.closeSearch();
|
|
182
199
|
return;
|
|
183
200
|
}
|
|
184
201
|
if (this.keybindings.matches(key, "searchToggleMode")) {
|
|
@@ -202,7 +219,7 @@ export class TuiController {
|
|
|
202
219
|
const selected = this.state.searchResults[this.state.itemIndex];
|
|
203
220
|
if (selected) {
|
|
204
221
|
// 有选中项:打开
|
|
205
|
-
this.
|
|
222
|
+
this.restoreSearchOriginForActivation();
|
|
206
223
|
void this.activateContentItem(selected, this.nextSignal());
|
|
207
224
|
}
|
|
208
225
|
else if (this.state.searchQuery.trim()) {
|
|
@@ -389,7 +406,7 @@ export class TuiController {
|
|
|
389
406
|
}
|
|
390
407
|
// r:刷新
|
|
391
408
|
if (this.keybindings.matches(key, "topicRefresh") && this.state.topic) {
|
|
392
|
-
void this.
|
|
409
|
+
void this.refreshCurrentTopic(this.nextSignal());
|
|
393
410
|
return;
|
|
394
411
|
}
|
|
395
412
|
// s:收藏
|
|
@@ -420,11 +437,11 @@ export class TuiController {
|
|
|
420
437
|
this.openMenu();
|
|
421
438
|
}
|
|
422
439
|
}
|
|
423
|
-
// c
|
|
440
|
+
// c:图片复制图片本体,链接复制 URL
|
|
424
441
|
if (this.keybindings.matches(key, "topicCopyLink")) {
|
|
425
442
|
const currentLine = this.getCurrentTopicLine();
|
|
426
443
|
if (currentLine?.kind === "image" && currentLine.imageUrl) {
|
|
427
|
-
void this.
|
|
444
|
+
void this.copyImageToClipboard(currentLine.imageUrl);
|
|
428
445
|
}
|
|
429
446
|
else if (currentLine?.kind === "link" && currentLine.linkUrl) {
|
|
430
447
|
void this.copyToClipboard(currentLine.linkUrl);
|
|
@@ -432,8 +449,9 @@ export class TuiController {
|
|
|
432
449
|
}
|
|
433
450
|
}
|
|
434
451
|
handleSettingsKey(key) {
|
|
452
|
+
const itemCount = this.state.items.length || settingsItems.length;
|
|
435
453
|
if (this.keybindings.matches(key, "moveDown")) {
|
|
436
|
-
this.state.itemIndex = Math.min(
|
|
454
|
+
this.state.itemIndex = Math.min(itemCount - 1, this.state.itemIndex + 1);
|
|
437
455
|
this.render();
|
|
438
456
|
return;
|
|
439
457
|
}
|
|
@@ -450,7 +468,7 @@ export class TuiController {
|
|
|
450
468
|
return;
|
|
451
469
|
}
|
|
452
470
|
if (this.keybindings.matches(key, "confirm") || this.keybindings.matches(key, "moveRight")) {
|
|
453
|
-
void this.activateSetting(settingsItems[this.state.itemIndex]);
|
|
471
|
+
void this.activateSetting(this.state.items[this.state.itemIndex] ?? settingsItems[this.state.itemIndex]);
|
|
454
472
|
}
|
|
455
473
|
}
|
|
456
474
|
handleNavKey(key) {
|
|
@@ -479,9 +497,20 @@ export class TuiController {
|
|
|
479
497
|
void this.load(true);
|
|
480
498
|
}
|
|
481
499
|
handleContentKey(key) {
|
|
500
|
+
if ((key === "\t" || key === "\x1b[Z") && this.state.tabs.length > 1) {
|
|
501
|
+
this.switchTab(key === "\x1b[Z" ? -1 : 1);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (/^[1-9]$/.test(key) && this.state.tabs.length > 1) {
|
|
505
|
+
this.switchTabToIndex(Number(key) - 1);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
482
508
|
if (this.keybindings.matches(key, "listNext")) {
|
|
509
|
+
const previousIndex = this.state.itemIndex;
|
|
510
|
+
const shouldAdvanceAfterLoad = previousIndex >= this.state.items.length - 1;
|
|
483
511
|
this.state.itemIndex = Math.min(Math.max(0, this.state.items.length - 1), this.state.itemIndex + 1);
|
|
484
512
|
this.render();
|
|
513
|
+
void this.checkListAutoLoad(shouldAdvanceAfterLoad ? previousIndex + 1 : undefined);
|
|
485
514
|
return;
|
|
486
515
|
}
|
|
487
516
|
if (this.keybindings.matches(key, "listPrev")) {
|
|
@@ -511,13 +540,22 @@ export class TuiController {
|
|
|
511
540
|
if (this.keybindings.matches(key, "menu"))
|
|
512
541
|
this.openMenu();
|
|
513
542
|
}
|
|
543
|
+
switchTab(delta) {
|
|
544
|
+
const current = Math.max(0, this.state.tabs.findIndex((tab) => tab.id === this.state.tabId));
|
|
545
|
+
const next = (current + delta + this.state.tabs.length) % this.state.tabs.length;
|
|
546
|
+
this.switchTabToIndex(next);
|
|
547
|
+
}
|
|
548
|
+
switchTabToIndex(index) {
|
|
549
|
+
const tab = this.state.tabs[index];
|
|
550
|
+
if (!tab || tab.id === this.state.tabId)
|
|
551
|
+
return;
|
|
552
|
+
this.state.tabId = tab.id;
|
|
553
|
+
void this.load(true);
|
|
554
|
+
}
|
|
514
555
|
leave() {
|
|
515
556
|
this.abortCurrent();
|
|
516
557
|
if (this.state.mode === "topic") {
|
|
517
|
-
this.
|
|
518
|
-
this.state.focus = "content";
|
|
519
|
-
this.state.status = "";
|
|
520
|
-
this.render();
|
|
558
|
+
void this.leaveTopic();
|
|
521
559
|
return;
|
|
522
560
|
}
|
|
523
561
|
if (this.state.parentList) {
|
|
@@ -529,8 +567,139 @@ export class TuiController {
|
|
|
529
567
|
this.state.status = "";
|
|
530
568
|
this.render();
|
|
531
569
|
}
|
|
570
|
+
rememberListReturnState() {
|
|
571
|
+
if (this.state.mode !== "list")
|
|
572
|
+
return;
|
|
573
|
+
this.listReturnState = {
|
|
574
|
+
itemIndex: this.state.itemIndex,
|
|
575
|
+
scroll: this.state.scroll,
|
|
576
|
+
paging: this.cloneListPaging(this.state.listPaging)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async leaveTopic() {
|
|
580
|
+
const listReturn = this.listReturnState;
|
|
581
|
+
this.state.mode = "list";
|
|
582
|
+
this.state.focus = "content";
|
|
583
|
+
this.state.status = "";
|
|
584
|
+
this.state.topic = undefined;
|
|
585
|
+
if (listReturn) {
|
|
586
|
+
this.state.itemIndex = Math.min(Math.max(0, this.state.items.length - 1), listReturn.itemIndex);
|
|
587
|
+
this.state.listPaging = this.cloneListPaging(listReturn.paging);
|
|
588
|
+
this.state.scroll = listReturn.paging?.anchorOnReturn ? this.state.itemIndex : listReturn.scroll;
|
|
589
|
+
this.listReturnState = undefined;
|
|
590
|
+
}
|
|
591
|
+
this.render();
|
|
592
|
+
if (this.state.listPaging?.anchorOnReturn) {
|
|
593
|
+
await this.ensureListWindowFilled(this.nextSignal());
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async ensureListWindowFilled(signal) {
|
|
597
|
+
const paging = this.state.listPaging;
|
|
598
|
+
if (!paging?.hasMore || this.state.loadingMore)
|
|
599
|
+
return;
|
|
600
|
+
const capacity = Math.max(1, this.state.listViewportCapacity || 10);
|
|
601
|
+
const targetLength = this.state.scroll + capacity;
|
|
602
|
+
while (this.state.items.length < targetLength && paging.hasMore && !this.state.loadingMore) {
|
|
603
|
+
const previousLength = this.state.items.length;
|
|
604
|
+
await this.loadNextListPage(signal);
|
|
605
|
+
if (this.state.items.length === previousLength)
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async checkListAutoLoad(advanceToIndex) {
|
|
610
|
+
const paging = this.state.listPaging;
|
|
611
|
+
if (!paging?.hasMore || this.state.loadingMore)
|
|
612
|
+
return;
|
|
613
|
+
const capacity = Math.max(1, this.state.listViewportCapacity || 10);
|
|
614
|
+
const visibleEnd = this.state.scroll + capacity;
|
|
615
|
+
if (this.state.items.length <= visibleEnd || this.state.itemIndex >= this.state.items.length - 2) {
|
|
616
|
+
await this.loadNextListPage(this.nextSignal());
|
|
617
|
+
if (advanceToIndex !== undefined && advanceToIndex < this.state.items.length) {
|
|
618
|
+
this.state.itemIndex = advanceToIndex;
|
|
619
|
+
this.state.status = "";
|
|
620
|
+
this.render();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async loadNextListPage(signal) {
|
|
625
|
+
const paging = this.state.listPaging;
|
|
626
|
+
if (!paging?.hasMore || this.state.loadingMore)
|
|
627
|
+
return;
|
|
628
|
+
this.state.loadingMore = true;
|
|
629
|
+
this.render();
|
|
630
|
+
try {
|
|
631
|
+
const nextItems = await this.fetchNextListPage(paging, signal);
|
|
632
|
+
this.state.items.push(...nextItems);
|
|
633
|
+
paging.loaded += nextItems.length;
|
|
634
|
+
if (paging.kind !== "favorite-board-topics") {
|
|
635
|
+
paging.hasMore = nextItems.length >= paging.size;
|
|
636
|
+
}
|
|
637
|
+
this.state.stats = this.state.stats.map((item) => {
|
|
638
|
+
if (item.title === "版面主题" && paging.kind === "favorite-board-topics") {
|
|
639
|
+
return { ...item, detail: `${paging.loaded} 条` };
|
|
640
|
+
}
|
|
641
|
+
if (["主题", "新帖流", "最新新帖", "关注用户", "收藏更新"].includes(item.title)) {
|
|
642
|
+
return { ...item, detail: `${this.state.items.length} 条` };
|
|
643
|
+
}
|
|
644
|
+
return item;
|
|
645
|
+
});
|
|
646
|
+
this.state.status = paging.hasMore ? "已加载更多" : "已到底";
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
if (!isAbortError(error))
|
|
650
|
+
this.state.status = error instanceof Error ? error.message : "加载失败";
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
this.state.loadingMore = false;
|
|
654
|
+
this.render();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async fetchNextListPage(paging, signal) {
|
|
658
|
+
if (paging.kind === "new-topics") {
|
|
659
|
+
const topics = asArray(await this.client.getNewTopics(paging.loaded, paging.size, false, signal));
|
|
660
|
+
return topics.map((topic) => topicItem(topic));
|
|
661
|
+
}
|
|
662
|
+
if (paging.kind === "followee-topics") {
|
|
663
|
+
const topics = asArray(await this.client.getFolloweeTopics(paging.loaded, paging.size, false, signal));
|
|
664
|
+
return topics.map((topic) => topicItem(topic));
|
|
665
|
+
}
|
|
666
|
+
if (paging.kind === "favorite-board-topics") {
|
|
667
|
+
return this.fetchNextFavoriteBoardTopics(paging, signal);
|
|
668
|
+
}
|
|
669
|
+
const order = paging.kind === "favorite-updates" ? 1 : 0;
|
|
670
|
+
const topics = asArray(await this.client.getFavoriteTopics(paging.loaded, paging.size, order, 0, false, signal));
|
|
671
|
+
return topics.map((topic) => topicItem(topic));
|
|
672
|
+
}
|
|
673
|
+
async fetchNextFavoriteBoardTopics(paging, signal) {
|
|
674
|
+
const take = Math.max(1, Math.min(20, paging.size));
|
|
675
|
+
if ((paging.buffer?.length ?? 0) >= take) {
|
|
676
|
+
const items = paging.buffer?.splice(0, take) ?? [];
|
|
677
|
+
paging.hasMore = (paging.buffer?.length ?? 0) > 0 || (paging.boardCursors?.some((cursor) => cursor.hasMore) ?? false);
|
|
678
|
+
return items;
|
|
679
|
+
}
|
|
680
|
+
const cursors = paging.boardCursors?.filter((cursor) => cursor.hasMore) ?? [];
|
|
681
|
+
if (cursors.length === 0) {
|
|
682
|
+
const items = paging.buffer?.splice(0, take) ?? [];
|
|
683
|
+
paging.hasMore = (paging.buffer?.length ?? 0) > 0;
|
|
684
|
+
return items;
|
|
685
|
+
}
|
|
686
|
+
const batches = await mapLimit(cursors, 3, async (cursor) => {
|
|
687
|
+
const topics = asArray(await this.client.getBoardTopics(cursor.boardId, cursor.loaded, cursor.size, false, false, signal));
|
|
688
|
+
cursor.loaded += topics.length;
|
|
689
|
+
cursor.hasMore = topics.length >= cursor.size;
|
|
690
|
+
return topics.map((topic) => topicItem(topic, { title: cursor.title, boardId: cursor.boardId }));
|
|
691
|
+
});
|
|
692
|
+
const merged = [...(paging.buffer ?? []), ...batches.flat()]
|
|
693
|
+
.sort((left, right) => (right.sortTime ?? 0) - (left.sortTime ?? 0));
|
|
694
|
+
paging.buffer = merged;
|
|
695
|
+
paging.hasMore = paging.buffer.length > 0 || cursors.some((cursor) => cursor.hasMore);
|
|
696
|
+
return paging.buffer.splice(0, take);
|
|
697
|
+
}
|
|
532
698
|
refresh() {
|
|
533
|
-
if (this.state.
|
|
699
|
+
if (this.state.mode === "topic" && this.state.topic) {
|
|
700
|
+
void this.refreshCurrentTopic(this.nextSignal());
|
|
701
|
+
}
|
|
702
|
+
else if (this.state.currentBoard) {
|
|
534
703
|
void this.openBoard(this.state.currentBoard.boardId, this.state.currentBoard.title, true, this.nextSignal(), false);
|
|
535
704
|
}
|
|
536
705
|
else if (this.state.currentChat) {
|
|
@@ -542,6 +711,7 @@ export class TuiController {
|
|
|
542
711
|
}
|
|
543
712
|
openSearch() {
|
|
544
713
|
this.state.modal = "search";
|
|
714
|
+
this.state.searchOrigin = { itemIndex: this.state.itemIndex, scroll: this.state.scroll };
|
|
545
715
|
this.state.searchQuery = "";
|
|
546
716
|
this.state.searchResults = [];
|
|
547
717
|
this.state.searchMode = "topics";
|
|
@@ -549,6 +719,29 @@ export class TuiController {
|
|
|
549
719
|
this.state.itemIndex = 0;
|
|
550
720
|
this.render();
|
|
551
721
|
}
|
|
722
|
+
closeSearch() {
|
|
723
|
+
const origin = this.state.searchOrigin;
|
|
724
|
+
this.state.modal = null;
|
|
725
|
+
this.state.searchOrigin = undefined;
|
|
726
|
+
this.state.searchQuery = "";
|
|
727
|
+
this.state.searchResults = [];
|
|
728
|
+
if (origin) {
|
|
729
|
+
this.state.itemIndex = origin.itemIndex;
|
|
730
|
+
this.state.scroll = origin.scroll;
|
|
731
|
+
}
|
|
732
|
+
this.render();
|
|
733
|
+
}
|
|
734
|
+
restoreSearchOriginForActivation() {
|
|
735
|
+
const origin = this.state.searchOrigin;
|
|
736
|
+
this.state.modal = null;
|
|
737
|
+
this.state.searchOrigin = undefined;
|
|
738
|
+
this.state.searchQuery = "";
|
|
739
|
+
this.state.searchResults = [];
|
|
740
|
+
if (origin) {
|
|
741
|
+
this.state.itemIndex = origin.itemIndex;
|
|
742
|
+
this.state.scroll = origin.scroll;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
552
745
|
getSearchScope() {
|
|
553
746
|
// 根据当前位置确定搜索范围
|
|
554
747
|
if (this.state.currentBoard) {
|
|
@@ -584,9 +777,19 @@ export class TuiController {
|
|
|
584
777
|
this.state.userDetail = undefined;
|
|
585
778
|
this.render();
|
|
586
779
|
}
|
|
780
|
+
setTabs(tabs, defaultId) {
|
|
781
|
+
this.state.tabs = tabs;
|
|
782
|
+
if (!tabs.some((tab) => tab.id === this.state.tabId)) {
|
|
783
|
+
this.state.tabId = defaultId;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
587
786
|
async loadView(view, force, signal) {
|
|
787
|
+
if (view !== "new" && view !== "following") {
|
|
788
|
+
this.setTabs([{ id: "default", label: "" }], "default");
|
|
789
|
+
}
|
|
588
790
|
switch (view) {
|
|
589
791
|
case "hot": {
|
|
792
|
+
this.setTabs([{ id: "default", label: "" }], "default");
|
|
590
793
|
const [index, unread] = await Promise.all([
|
|
591
794
|
this.client.getForumIndex(force, signal),
|
|
592
795
|
this.client.getUnreadCount(force, signal)
|
|
@@ -598,14 +801,46 @@ export class TuiController {
|
|
|
598
801
|
title: "十大",
|
|
599
802
|
items: hotTopics.map((topic) => topicItem(topic)),
|
|
600
803
|
stats: unreadStats(unreadObject),
|
|
601
|
-
overview: overviewStats(indexObject, unreadObject)
|
|
804
|
+
overview: overviewStats(indexObject, unreadObject),
|
|
805
|
+
status: "十大:j/k 选择 Enter 打开 r 刷新"
|
|
602
806
|
};
|
|
603
807
|
}
|
|
604
808
|
case "new": {
|
|
605
|
-
|
|
606
|
-
|
|
809
|
+
this.setTabs([
|
|
810
|
+
{ id: "new-latest", label: "最新" },
|
|
811
|
+
{ id: "new-random", label: "随机" },
|
|
812
|
+
{ id: "new-recommendation", label: "推荐" }
|
|
813
|
+
], "new-latest");
|
|
814
|
+
if (this.state.tabId === "new-recommendation") {
|
|
815
|
+
const topics = asArray(await this.client.getRandomRecommendations(10, true, signal));
|
|
816
|
+
return {
|
|
817
|
+
title: "新帖 · 推荐",
|
|
818
|
+
items: topics.map((topic) => topicItem(topic)),
|
|
819
|
+
stats: [{ title: "推荐", detail: `${topics.length} 条` }],
|
|
820
|
+
status: "新帖:Tab 切换 j/k 选择 Enter 打开 r 换一批"
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (this.state.tabId === "new-random") {
|
|
824
|
+
const topics = asArray(await this.client.getRandomTopics(20, true, signal));
|
|
825
|
+
return {
|
|
826
|
+
title: "新帖 · 随机",
|
|
827
|
+
items: topics.map((topic) => topicItem(topic)),
|
|
828
|
+
stats: [{ title: "随机新帖", detail: `${topics.length} 条` }],
|
|
829
|
+
status: "新帖:Tab 切换 j/k 选择 Enter 打开 r 换一批"
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const size = 20;
|
|
833
|
+
const topics = asArray(await this.client.getNewTopics(0, size, force, signal));
|
|
834
|
+
return {
|
|
835
|
+
title: "新帖 · 最新",
|
|
836
|
+
items: topics.map((topic) => topicItem(topic)),
|
|
837
|
+
stats: [{ title: "最新新帖", detail: `${topics.length} 条` }],
|
|
838
|
+
status: "新帖:Tab 切换 j/k 选择 Enter 打开 r 刷新",
|
|
839
|
+
paging: { kind: "new-topics", loaded: topics.length, size, hasMore: topics.length >= size, anchorOnReturn: true }
|
|
840
|
+
};
|
|
607
841
|
}
|
|
608
842
|
case "boards": {
|
|
843
|
+
this.setTabs([{ id: "default", label: "" }], "default");
|
|
609
844
|
const sections = asArray(await this.client.getAllBoards(force, signal));
|
|
610
845
|
const boards = flattenBoards(sections);
|
|
611
846
|
return {
|
|
@@ -616,15 +851,37 @@ export class TuiController {
|
|
|
616
851
|
};
|
|
617
852
|
}
|
|
618
853
|
case "following": {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
854
|
+
this.setTabs([
|
|
855
|
+
{ id: "follow-boards", label: "版面" },
|
|
856
|
+
{ id: "follow-users", label: "用户" },
|
|
857
|
+
{ id: "follow-favorites", label: "追踪" }
|
|
858
|
+
], "follow-boards");
|
|
859
|
+
if (this.state.tabId === "follow-users") {
|
|
860
|
+
const size = 20;
|
|
861
|
+
const topics = asArray(await this.client.getFolloweeTopics(0, size, force, signal));
|
|
862
|
+
return {
|
|
863
|
+
title: "关注 · 用户",
|
|
864
|
+
items: topics.map((topic) => topicItem(topic)),
|
|
865
|
+
stats: [{ title: "关注用户", detail: `${topics.length} 条` }],
|
|
866
|
+
status: "关注:Tab 切换 j/k 选择 Enter 打开 r 刷新",
|
|
867
|
+
paging: { kind: "followee-topics", loaded: topics.length, size, hasMore: topics.length >= size, anchorOnReturn: true }
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (this.state.tabId === "follow-favorites") {
|
|
871
|
+
const size = 20;
|
|
872
|
+
const topics = asArray(await this.client.getFavoriteTopics(0, size, 1, 0, force, signal));
|
|
873
|
+
return {
|
|
874
|
+
title: "关注 · 追踪",
|
|
875
|
+
items: topics.map((topic) => topicItem(topic)),
|
|
876
|
+
stats: [{ title: "收藏更新", detail: `${topics.length} 条` }],
|
|
877
|
+
status: "关注:Tab 切换 j/k 选择 Enter 打开 r 刷新",
|
|
878
|
+
paging: { kind: "favorite-updates", loaded: topics.length, size, hasMore: topics.length >= size, anchorOnReturn: true }
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
return this.loadFavoriteBoardTopics(force, signal);
|
|
626
882
|
}
|
|
627
883
|
case "favorite": {
|
|
884
|
+
this.setTabs([{ id: "default", label: "" }], "default");
|
|
628
885
|
const [meRaw, sectionsRaw, topicFavorites] = await Promise.all([
|
|
629
886
|
this.client.getMe(force, signal),
|
|
630
887
|
this.client.getAllBoards(false, signal),
|
|
@@ -633,12 +890,20 @@ export class TuiController {
|
|
|
633
890
|
const customBoards = asArray(asObject(meRaw).customBoards).filter((id) => typeof id === "number");
|
|
634
891
|
const allBoards = flattenBoards(asArray(sectionsRaw));
|
|
635
892
|
const boardById = new Map(allBoards.filter((board) => board.boardId !== undefined).map((board) => [board.boardId, board]));
|
|
893
|
+
const boardPageSize = 5;
|
|
636
894
|
const topicGroups = await mapLimit(customBoards, 3, async (boardId) => {
|
|
637
895
|
const board = boardById.get(boardId);
|
|
638
|
-
const topics = asArray(await this.client.getBoardTopics(boardId, 0,
|
|
639
|
-
return topics.map((topic) => topicItem(topic, board));
|
|
896
|
+
const topics = asArray(await this.client.getBoardTopics(boardId, 0, boardPageSize, false, force, signal));
|
|
897
|
+
return { boardId, board, topics: topics.map((topic) => topicItem(topic, board)), hasMore: topics.length >= boardPageSize };
|
|
640
898
|
});
|
|
641
|
-
const boardTopics = topicGroups.
|
|
899
|
+
const boardTopics = topicGroups.flatMap((group) => group.topics).sort((left, right) => (right.sortTime ?? 0) - (left.sortTime ?? 0));
|
|
900
|
+
const boardCursors = topicGroups.map((group) => ({
|
|
901
|
+
boardId: group.boardId,
|
|
902
|
+
title: group.board?.title ?? `#${group.boardId}`,
|
|
903
|
+
loaded: group.topics.length,
|
|
904
|
+
size: boardPageSize,
|
|
905
|
+
hasMore: group.hasMore
|
|
906
|
+
}));
|
|
642
907
|
return {
|
|
643
908
|
title: "收藏",
|
|
644
909
|
items: [
|
|
@@ -653,7 +918,15 @@ export class TuiController {
|
|
|
653
918
|
{ title: "收藏主题", detail: `${asArray(topicFavorites).length} 条` },
|
|
654
919
|
{ title: "版面主题", detail: `${boardTopics.length} 条` }
|
|
655
920
|
],
|
|
656
|
-
status: "收藏:j/k 选择 Enter 打开 h 返回 r 刷新"
|
|
921
|
+
status: "收藏:j/k 选择 Enter 打开 h 返回 r 刷新",
|
|
922
|
+
paging: {
|
|
923
|
+
kind: "favorite-board-topics",
|
|
924
|
+
loaded: boardTopics.length,
|
|
925
|
+
size: Math.max(12, customBoards.length * boardPageSize),
|
|
926
|
+
hasMore: boardCursors.some((cursor) => cursor.hasMore),
|
|
927
|
+
anchorOnReturn: true,
|
|
928
|
+
boardCursors
|
|
929
|
+
}
|
|
657
930
|
};
|
|
658
931
|
}
|
|
659
932
|
case "messages": {
|
|
@@ -705,16 +978,63 @@ export class TuiController {
|
|
|
705
978
|
};
|
|
706
979
|
}
|
|
707
980
|
case "settings": {
|
|
708
|
-
const cacheStats = await
|
|
981
|
+
const [cacheStats, autoSignin] = await Promise.all([
|
|
982
|
+
this.client.getCacheStats(),
|
|
983
|
+
this.settingsStore.isAutoSigninEnabled()
|
|
984
|
+
]);
|
|
709
985
|
return {
|
|
710
986
|
title: "设置",
|
|
711
|
-
items:
|
|
712
|
-
stats: [
|
|
987
|
+
items: this.renderSettingsItems(autoSignin),
|
|
988
|
+
stats: [
|
|
989
|
+
{ title: "自动签到", detail: autoSignin ? "已开启" : "已关闭" },
|
|
990
|
+
{ title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` },
|
|
991
|
+
{ title: "版本", detail: `v${appVersion}` }
|
|
992
|
+
],
|
|
713
993
|
status: "设置:j/k 选择 Enter 执行 h 返回"
|
|
714
994
|
};
|
|
715
995
|
}
|
|
716
996
|
}
|
|
717
997
|
}
|
|
998
|
+
async loadFavoriteBoardTopics(force, signal) {
|
|
999
|
+
const [meRaw, sectionsRaw] = await Promise.all([
|
|
1000
|
+
this.client.getMe(force, signal),
|
|
1001
|
+
this.client.getAllBoards(false, signal)
|
|
1002
|
+
]);
|
|
1003
|
+
const customBoards = asArray(asObject(meRaw).customBoards).filter((id) => typeof id === "number");
|
|
1004
|
+
const allBoards = flattenBoards(asArray(sectionsRaw));
|
|
1005
|
+
const boardById = new Map(allBoards.filter((board) => board.boardId !== undefined).map((board) => [board.boardId, board]));
|
|
1006
|
+
const boardPageSize = 5;
|
|
1007
|
+
const topicGroups = await mapLimit(customBoards, 3, async (boardId) => {
|
|
1008
|
+
const board = boardById.get(boardId);
|
|
1009
|
+
const topics = asArray(await this.client.getBoardTopics(boardId, 0, boardPageSize, false, force, signal));
|
|
1010
|
+
return { boardId, board, topics: topics.map((topic) => topicItem(topic, board)), hasMore: topics.length >= boardPageSize };
|
|
1011
|
+
});
|
|
1012
|
+
const boardTopics = topicGroups.flatMap((group) => group.topics).sort((left, right) => (right.sortTime ?? 0) - (left.sortTime ?? 0));
|
|
1013
|
+
const boardCursors = topicGroups.map((group) => ({
|
|
1014
|
+
boardId: group.boardId,
|
|
1015
|
+
title: group.board?.title ?? `#${group.boardId}`,
|
|
1016
|
+
loaded: group.topics.length,
|
|
1017
|
+
size: boardPageSize,
|
|
1018
|
+
hasMore: group.hasMore
|
|
1019
|
+
}));
|
|
1020
|
+
return {
|
|
1021
|
+
title: "关注 · 版面",
|
|
1022
|
+
items: boardTopics,
|
|
1023
|
+
stats: [
|
|
1024
|
+
{ title: "关注版面", detail: `${customBoards.length} 个` },
|
|
1025
|
+
{ title: "版面主题", detail: `${boardTopics.length} 条` }
|
|
1026
|
+
],
|
|
1027
|
+
status: "关注:Tab 切换 j/k 选择 Enter 打开 r 刷新",
|
|
1028
|
+
paging: {
|
|
1029
|
+
kind: "favorite-board-topics",
|
|
1030
|
+
loaded: boardTopics.length,
|
|
1031
|
+
size: Math.max(12, customBoards.length * boardPageSize),
|
|
1032
|
+
hasMore: boardCursors.some((cursor) => cursor.hasMore),
|
|
1033
|
+
anchorOnReturn: true,
|
|
1034
|
+
boardCursors
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
718
1038
|
async activateSetting(selected) {
|
|
719
1039
|
if (!selected)
|
|
720
1040
|
return;
|
|
@@ -731,6 +1051,14 @@ export class TuiController {
|
|
|
731
1051
|
void this.openCacheManager();
|
|
732
1052
|
return;
|
|
733
1053
|
}
|
|
1054
|
+
if (selected.meta === "pixel-logo") {
|
|
1055
|
+
this.openPixelLogo();
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (selected.meta === "emoji-preview") {
|
|
1059
|
+
this.openEmojiPreview();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
734
1062
|
if (selected.meta === "update") {
|
|
735
1063
|
void this.checkUpdate(true);
|
|
736
1064
|
return;
|
|
@@ -739,6 +1067,10 @@ export class TuiController {
|
|
|
739
1067
|
void this.openAccountSwitcher();
|
|
740
1068
|
return;
|
|
741
1069
|
}
|
|
1070
|
+
if (selected.meta === "auto-signin") {
|
|
1071
|
+
void this.toggleAutoSignin();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
742
1074
|
if (selected.meta === "logout") {
|
|
743
1075
|
void this.confirmLogout();
|
|
744
1076
|
return;
|
|
@@ -746,8 +1078,67 @@ export class TuiController {
|
|
|
746
1078
|
this.state.status = "功能开发中...";
|
|
747
1079
|
this.render();
|
|
748
1080
|
}
|
|
1081
|
+
renderSettingsItems(autoSignin) {
|
|
1082
|
+
return settingsItems.map((item) => {
|
|
1083
|
+
if (item.meta !== "auto-signin") {
|
|
1084
|
+
return { ...item };
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
...item,
|
|
1088
|
+
title: `自动签到: ${autoSignin ? "开启" : "关闭"}`,
|
|
1089
|
+
detail: autoSignin
|
|
1090
|
+
? "启动后为所有账号执行每日签到"
|
|
1091
|
+
: "默认关闭,启动时不自动签到"
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
async toggleAutoSignin() {
|
|
1096
|
+
const enabled = await this.settingsStore.isAutoSigninEnabled();
|
|
1097
|
+
const next = !enabled;
|
|
1098
|
+
await this.settingsStore.setAutoSigninEnabled(next);
|
|
1099
|
+
this.state.items = this.renderSettingsItems(next);
|
|
1100
|
+
this.state.stats = [
|
|
1101
|
+
{ title: "自动签到", detail: next ? "已开启" : "已关闭" },
|
|
1102
|
+
...this.state.stats.filter((item) => item.title !== "自动签到")
|
|
1103
|
+
];
|
|
1104
|
+
this.state.status = next ? "已开启自动签到" : "已关闭自动签到";
|
|
1105
|
+
this.render();
|
|
1106
|
+
}
|
|
1107
|
+
async runAutoSignin() {
|
|
1108
|
+
const enabled = await this.settingsStore.isAutoSigninEnabled();
|
|
1109
|
+
if (!enabled)
|
|
1110
|
+
return;
|
|
1111
|
+
const accounts = await this.tokenStore.listAccounts();
|
|
1112
|
+
if (accounts.length === 0)
|
|
1113
|
+
return;
|
|
1114
|
+
let success = 0;
|
|
1115
|
+
let failed = 0;
|
|
1116
|
+
this.state.status = `自动签到: 0/${accounts.length}`;
|
|
1117
|
+
this.render();
|
|
1118
|
+
for (const account of accounts) {
|
|
1119
|
+
try {
|
|
1120
|
+
const tokenStore = this.tokenStore.withAccount(account.account);
|
|
1121
|
+
const client = new Cc98Client({ tokenStore, webVpn: this.webVpnOptions });
|
|
1122
|
+
if (this.webVpnOptions) {
|
|
1123
|
+
await client.initWebVpn();
|
|
1124
|
+
}
|
|
1125
|
+
await client.signin();
|
|
1126
|
+
success += 1;
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
failed += 1;
|
|
1130
|
+
}
|
|
1131
|
+
this.state.status = `自动签到: ${success + failed}/${accounts.length}`;
|
|
1132
|
+
this.render();
|
|
1133
|
+
}
|
|
1134
|
+
this.state.status = failed > 0
|
|
1135
|
+
? `自动签到完成: ${success} 成功,${failed} 失败`
|
|
1136
|
+
: `自动签到完成: ${success} 个账号`;
|
|
1137
|
+
this.render();
|
|
1138
|
+
}
|
|
749
1139
|
async activateContentItem(selected, signal) {
|
|
750
1140
|
if (selected.topicId !== undefined) {
|
|
1141
|
+
this.rememberListReturnState();
|
|
751
1142
|
await this.openTopic(selected.topicId, false, signal);
|
|
752
1143
|
return;
|
|
753
1144
|
}
|
|
@@ -769,6 +1160,14 @@ export class TuiController {
|
|
|
769
1160
|
await this.switchAccount(accountName);
|
|
770
1161
|
return;
|
|
771
1162
|
}
|
|
1163
|
+
if (selected.meta?.startsWith("emoji-category:")) {
|
|
1164
|
+
this.openEmojiCategory(selected.meta.slice("emoji-category:".length));
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (selected.meta?.startsWith("emoji:")) {
|
|
1168
|
+
this.openEmojiDetail(selected.meta.slice("emoji:".length));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
772
1171
|
if (selected.action?.startsWith("notices:")) {
|
|
773
1172
|
await this.openNoticeList(selected.action.split(":")[1], signal);
|
|
774
1173
|
return;
|
|
@@ -780,7 +1179,7 @@ export class TuiController {
|
|
|
780
1179
|
this.state.status = "当前条目不可进入";
|
|
781
1180
|
this.render();
|
|
782
1181
|
}
|
|
783
|
-
async openTopic(topicId, force, signal) {
|
|
1182
|
+
async openTopic(topicId, force, signal, restore) {
|
|
784
1183
|
this.state.mode = "topic";
|
|
785
1184
|
this.state.loading = true;
|
|
786
1185
|
this.state.error = undefined;
|
|
@@ -789,11 +1188,15 @@ export class TuiController {
|
|
|
789
1188
|
this.state.scroll = 0;
|
|
790
1189
|
this.render();
|
|
791
1190
|
try {
|
|
1191
|
+
const from = restore ? Math.max(0, restore.floor - 1) : 0;
|
|
1192
|
+
const size = 10;
|
|
792
1193
|
const [topicRaw, postsRaw] = await Promise.all([
|
|
793
1194
|
this.client.getTopic(topicId, force, signal),
|
|
794
|
-
this.client.getTopicPosts(topicId,
|
|
1195
|
+
this.client.getTopicPosts(topicId, from, size, force, signal)
|
|
795
1196
|
]);
|
|
796
|
-
this.state.topic = buildTopicReader(topicId, asObject(topicRaw), asArray(postsRaw),
|
|
1197
|
+
this.state.topic = buildTopicReader(topicId, asObject(topicRaw), asArray(postsRaw), size, from);
|
|
1198
|
+
if (restore)
|
|
1199
|
+
this.restoreTopicPosition(restore);
|
|
797
1200
|
this.state.loading = false;
|
|
798
1201
|
this.state.status = "";
|
|
799
1202
|
}
|
|
@@ -804,6 +1207,35 @@ export class TuiController {
|
|
|
804
1207
|
}
|
|
805
1208
|
}
|
|
806
1209
|
this.render();
|
|
1210
|
+
// Start background image preloading so preview/open/copy can reuse the local cache.
|
|
1211
|
+
if (this.state.topic) {
|
|
1212
|
+
void this.preloadTopicImages(this.state.topic);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async refreshCurrentTopic(signal) {
|
|
1216
|
+
const topic = this.state.topic;
|
|
1217
|
+
if (!topic)
|
|
1218
|
+
return;
|
|
1219
|
+
const restore = this.getTopicRestoreTarget(topic);
|
|
1220
|
+
await this.openTopic(topic.topicId, true, signal, restore);
|
|
1221
|
+
}
|
|
1222
|
+
getTopicRestoreTarget(topic) {
|
|
1223
|
+
const post = currentTopicPost(topic, topic.cursorLine);
|
|
1224
|
+
return {
|
|
1225
|
+
floor: post?.floor ?? 1,
|
|
1226
|
+
lineOffset: post ? Math.max(0, topic.cursorLine - post.lineStart) : 0,
|
|
1227
|
+
loaded: topic.loaded
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
restoreTopicPosition(target) {
|
|
1231
|
+
const topic = this.state.topic;
|
|
1232
|
+
if (!topic)
|
|
1233
|
+
return;
|
|
1234
|
+
const post = findTopicPostByFloor(topic, target.floor);
|
|
1235
|
+
if (!post)
|
|
1236
|
+
return;
|
|
1237
|
+
topic.cursorLine = Math.min(post.lineEnd, post.lineStart + target.lineOffset);
|
|
1238
|
+
this.state.scroll = topic.cursorLine;
|
|
807
1239
|
}
|
|
808
1240
|
async openBoard(boardId, title, force, signal, pushParent = true) {
|
|
809
1241
|
if (pushParent)
|
|
@@ -813,6 +1245,7 @@ export class TuiController {
|
|
|
813
1245
|
this.state.viewTitle = title;
|
|
814
1246
|
this.state.focus = "content";
|
|
815
1247
|
this.state.currentBoard = { boardId, title };
|
|
1248
|
+
this.state.listPaging = undefined;
|
|
816
1249
|
this.state.itemIndex = 0;
|
|
817
1250
|
this.state.scroll = 0;
|
|
818
1251
|
this.render();
|
|
@@ -838,6 +1271,7 @@ export class TuiController {
|
|
|
838
1271
|
this.state.error = undefined;
|
|
839
1272
|
this.state.viewTitle = `私信: ${title}`;
|
|
840
1273
|
this.state.focus = "content";
|
|
1274
|
+
this.state.listPaging = undefined;
|
|
841
1275
|
this.state.itemIndex = 0;
|
|
842
1276
|
this.state.scroll = 0;
|
|
843
1277
|
this.render();
|
|
@@ -934,9 +1368,7 @@ export class TuiController {
|
|
|
934
1368
|
return;
|
|
935
1369
|
const pageInfo = getTopicPageInfo(topic, topic.cursorLine);
|
|
936
1370
|
if (pageInfo.currentPage > 1) {
|
|
937
|
-
|
|
938
|
-
this.state.status = "";
|
|
939
|
-
this.render();
|
|
1371
|
+
await this.jumpToTopicPage(pageInfo.currentPage - 1, this.nextSignal());
|
|
940
1372
|
}
|
|
941
1373
|
}
|
|
942
1374
|
async jumpToTopicPage(page, signal) {
|
|
@@ -949,13 +1381,9 @@ export class TuiController {
|
|
|
949
1381
|
this.render();
|
|
950
1382
|
return;
|
|
951
1383
|
}
|
|
952
|
-
// 如果目标页未加载,需要加载到该页
|
|
953
1384
|
const targetFloor = (page - 1) * FLOORS_PER_PAGE + 1;
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
await this.loadNextTopicPage(signal, true);
|
|
957
|
-
if (topic.loaded === previousLoaded)
|
|
958
|
-
break;
|
|
1385
|
+
if (!findTopicPostByFloor(topic, targetFloor)) {
|
|
1386
|
+
await this.loadTopicWindow(targetFloor, signal);
|
|
959
1387
|
}
|
|
960
1388
|
const post = findTopicPostByFloor(topic, targetFloor);
|
|
961
1389
|
if (post) {
|
|
@@ -967,6 +1395,30 @@ export class TuiController {
|
|
|
967
1395
|
}
|
|
968
1396
|
this.render();
|
|
969
1397
|
}
|
|
1398
|
+
async loadTopicWindow(startFloor, signal) {
|
|
1399
|
+
const topic = this.state.topic;
|
|
1400
|
+
if (!topic || this.state.loadingMore)
|
|
1401
|
+
return;
|
|
1402
|
+
const from = Math.max(0, startFloor - 1);
|
|
1403
|
+
this.state.loadingMore = true;
|
|
1404
|
+
this.render();
|
|
1405
|
+
try {
|
|
1406
|
+
const posts = asArray(await this.client.getTopicPosts(topic.topicId, from, topic.size, false, signal));
|
|
1407
|
+
replaceTopicPosts(topic, posts, from);
|
|
1408
|
+
topic.hasMore = from + posts.length < topic.totalFloors;
|
|
1409
|
+
this.state.scroll = 0;
|
|
1410
|
+
this.state.status = "";
|
|
1411
|
+
void this.preloadTopicImages(topic);
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
if (!isAbortError(error))
|
|
1415
|
+
this.state.status = error instanceof Error ? error.message : "加载失败";
|
|
1416
|
+
}
|
|
1417
|
+
finally {
|
|
1418
|
+
this.state.loadingMore = false;
|
|
1419
|
+
this.render();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
970
1422
|
async loadNextTopicPage(signal, quiet = false) {
|
|
971
1423
|
const topic = this.state.topic;
|
|
972
1424
|
if (!topic?.hasMore || this.state.loadingMore)
|
|
@@ -978,6 +1430,7 @@ export class TuiController {
|
|
|
978
1430
|
const posts = asArray(await this.client.getTopicPosts(topic.topicId, topic.loaded, topic.size, false, signal));
|
|
979
1431
|
appendTopicPosts(topic, posts);
|
|
980
1432
|
this.state.status = "";
|
|
1433
|
+
void this.preloadTopicImages(topic);
|
|
981
1434
|
}
|
|
982
1435
|
catch (error) {
|
|
983
1436
|
if (!isAbortError(error))
|
|
@@ -999,11 +1452,8 @@ export class TuiController {
|
|
|
999
1452
|
this.render();
|
|
1000
1453
|
return;
|
|
1001
1454
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
await this.loadNextTopicPage(signal, true);
|
|
1005
|
-
if (topic.loaded === previousLoaded)
|
|
1006
|
-
break;
|
|
1455
|
+
if (!findTopicPostByFloor(topic, floor)) {
|
|
1456
|
+
await this.loadTopicWindow(floor, signal);
|
|
1007
1457
|
}
|
|
1008
1458
|
const post = findTopicPostByFloor(topic, floor);
|
|
1009
1459
|
topic.cursorLine = post?.lineStart ?? topic.cursorLine;
|
|
@@ -1030,8 +1480,9 @@ export class TuiController {
|
|
|
1030
1480
|
case "favorite-topics":
|
|
1031
1481
|
case "favorite-updates": {
|
|
1032
1482
|
const order = action === "favorite-updates" ? 1 : 0;
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1483
|
+
const size = 20;
|
|
1484
|
+
const topics = asArray(await this.client.getFavoriteTopics(0, size, order, 0, false, signal));
|
|
1485
|
+
this.openReadOnlyList(action === "favorite-updates" ? "收藏更新" : "收藏主题", topics.map((topic) => topicItem(topic)), [{ title: "主题", detail: `${topics.length}` }], { kind: action === "favorite-updates" ? "favorite-updates" : "favorite-topics", loaded: topics.length, size, hasMore: topics.length >= size, anchorOnReturn: true });
|
|
1035
1486
|
return;
|
|
1036
1487
|
}
|
|
1037
1488
|
case "favorite-groups": {
|
|
@@ -1250,12 +1701,55 @@ export class TuiController {
|
|
|
1250
1701
|
const topic = this.state.topic;
|
|
1251
1702
|
if (!topic)
|
|
1252
1703
|
return undefined;
|
|
1704
|
+
return currentTopicLine(topic, topic.cursorLine);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Preload images for topic in background
|
|
1708
|
+
* Images are cached and trigger re-render when ready
|
|
1709
|
+
*/
|
|
1710
|
+
async preloadTopicImages(topic) {
|
|
1711
|
+
const cache = getImageCache();
|
|
1712
|
+
const imagesToLoad = new Set();
|
|
1713
|
+
// Collect all unique image URLs from posts
|
|
1253
1714
|
for (const post of topic.posts) {
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1715
|
+
for (const imageUrl of post.images) {
|
|
1716
|
+
if (imageUrl && !topic.imageCache.has(imageUrl) && !topic.imageLoading.has(imageUrl)) {
|
|
1717
|
+
imagesToLoad.add(imageUrl);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (imagesToLoad.size === 0)
|
|
1722
|
+
return;
|
|
1723
|
+
// Load images in parallel with concurrency limit
|
|
1724
|
+
const urls = Array.from(imagesToLoad);
|
|
1725
|
+
const concurrency = 3;
|
|
1726
|
+
for (let i = 0; i < urls.length; i += concurrency) {
|
|
1727
|
+
// Check if topic is still the same (user might have navigated away)
|
|
1728
|
+
if (this.state.topic !== topic)
|
|
1729
|
+
break;
|
|
1730
|
+
const batch = urls.slice(i, i + concurrency);
|
|
1731
|
+
let shouldRender = false;
|
|
1732
|
+
const promises = batch.map(async (url) => {
|
|
1733
|
+
topic.imageLoading.add(url);
|
|
1734
|
+
try {
|
|
1735
|
+
const localPath = await cache.getOrDownload(url);
|
|
1736
|
+
topic.imageErrors.delete(url);
|
|
1737
|
+
topic.imageCache.set(url, localPath);
|
|
1738
|
+
shouldRender = true;
|
|
1739
|
+
}
|
|
1740
|
+
catch (error) {
|
|
1741
|
+
topic.imageErrors.set(url, error instanceof Error ? error.message : "下载失败");
|
|
1742
|
+
shouldRender = true;
|
|
1743
|
+
}
|
|
1744
|
+
finally {
|
|
1745
|
+
topic.imageLoading.delete(url);
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
await Promise.all(promises);
|
|
1749
|
+
if (shouldRender && this.state.topic === topic) {
|
|
1750
|
+
this.render();
|
|
1751
|
+
}
|
|
1257
1752
|
}
|
|
1258
|
-
return undefined;
|
|
1259
1753
|
}
|
|
1260
1754
|
async openImage(url) {
|
|
1261
1755
|
this.state.status = "正在下载图片...";
|
|
@@ -1269,7 +1763,11 @@ export class TuiController {
|
|
|
1269
1763
|
const { execFile } = await import("node:child_process");
|
|
1270
1764
|
const platform = process.platform;
|
|
1271
1765
|
const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
1272
|
-
const args = platform === "win32"
|
|
1766
|
+
const args = platform === "win32"
|
|
1767
|
+
? ["/c", "start", "", localPath]
|
|
1768
|
+
: platform === "darwin"
|
|
1769
|
+
? ["-a", "Preview", localPath]
|
|
1770
|
+
: [localPath];
|
|
1273
1771
|
execFile(command, args, (error) => {
|
|
1274
1772
|
if (error) {
|
|
1275
1773
|
this.state.status = `打开失败: ${error.message}`;
|
|
@@ -1282,6 +1780,58 @@ export class TuiController {
|
|
|
1282
1780
|
this.render();
|
|
1283
1781
|
}
|
|
1284
1782
|
}
|
|
1783
|
+
async copyImageToClipboard(url) {
|
|
1784
|
+
this.state.status = "正在复制图片...";
|
|
1785
|
+
this.render();
|
|
1786
|
+
try {
|
|
1787
|
+
const cache = getImageCache();
|
|
1788
|
+
const localPath = await cache.getOrDownload(url);
|
|
1789
|
+
await this.copyImageFileToClipboard(localPath);
|
|
1790
|
+
this.state.status = "已复制图片到剪贴板";
|
|
1791
|
+
this.render();
|
|
1792
|
+
}
|
|
1793
|
+
catch (error) {
|
|
1794
|
+
this.state.status = error instanceof Error ? error.message : "复制图片失败";
|
|
1795
|
+
this.render();
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async copyImageFileToClipboard(localPath) {
|
|
1799
|
+
const platform = process.platform;
|
|
1800
|
+
if (platform === "darwin") {
|
|
1801
|
+
await this.copyImageFileToClipboardMac(localPath);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (platform === "win32") {
|
|
1805
|
+
const script = [
|
|
1806
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
1807
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
1808
|
+
"$image=[System.Drawing.Image]::FromFile($args[0])",
|
|
1809
|
+
"[System.Windows.Forms.Clipboard]::SetImage($image)",
|
|
1810
|
+
"$image.Dispose()"
|
|
1811
|
+
].join("; ");
|
|
1812
|
+
await execFilePromise("powershell.exe", ["-NoProfile", "-Command", script, localPath]);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const mime = imageMimeType(localPath);
|
|
1816
|
+
await execFilePromise("xclip", ["-selection", "clipboard", "-t", mime, localPath]);
|
|
1817
|
+
}
|
|
1818
|
+
async copyImageFileToClipboardMac(localPath) {
|
|
1819
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
1820
|
+
const { tmpdir } = await import("node:os");
|
|
1821
|
+
const { join } = await import("node:path");
|
|
1822
|
+
const dir = await mkdtemp(join(tmpdir(), "cc98-image-"));
|
|
1823
|
+
const tiffPath = join(dir, "clipboard.tiff");
|
|
1824
|
+
try {
|
|
1825
|
+
await execFilePromise("sips", ["-s", "format", "tiff", localPath, "--out", tiffPath]);
|
|
1826
|
+
await execFilePromise("osascript", [
|
|
1827
|
+
"-e",
|
|
1828
|
+
`set the clipboard to (read (POSIX file ${appleScriptString(tiffPath)}) as TIFF picture)`
|
|
1829
|
+
]);
|
|
1830
|
+
}
|
|
1831
|
+
finally {
|
|
1832
|
+
await rm(dir, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1285
1835
|
async copyToClipboard(text) {
|
|
1286
1836
|
try {
|
|
1287
1837
|
const { spawn } = await import("node:child_process");
|
|
@@ -1345,9 +1895,74 @@ export class TuiController {
|
|
|
1345
1895
|
this.state.infoLines = lines;
|
|
1346
1896
|
this.render();
|
|
1347
1897
|
}
|
|
1898
|
+
openPixelLogo() {
|
|
1899
|
+
this.state.modal = "info";
|
|
1900
|
+
this.state.infoTitle = "CC98 像素 Logo";
|
|
1901
|
+
this.state.infoLines = [
|
|
1902
|
+
...renderCc98Logo().split("\n"),
|
|
1903
|
+
"",
|
|
1904
|
+
"来源: https://www.cc98.org/static/images/98LOGO.ico",
|
|
1905
|
+
"渲染: 24-bit ANSI 半块像素"
|
|
1906
|
+
];
|
|
1907
|
+
this.render();
|
|
1908
|
+
}
|
|
1909
|
+
openEmojiPreview() {
|
|
1910
|
+
const items = EMOJI_CATEGORIES.map((category) => ({
|
|
1911
|
+
title: `${category.label} (${category.codes.length})`,
|
|
1912
|
+
meta: `emoji-category:${category.id}`,
|
|
1913
|
+
detail: `来源目录: Assets/Emoji/${category.source} · ${category.codes[0]} - ${category.codes.at(-1)}`
|
|
1914
|
+
}));
|
|
1915
|
+
this.openReadOnlyList("表情包预览", items, EMOJI_CATEGORIES.map((category) => ({
|
|
1916
|
+
title: category.label,
|
|
1917
|
+
detail: `${category.codes.length} 个`
|
|
1918
|
+
})));
|
|
1919
|
+
this.state.status = "表情包预览:j/k 选择分类 Enter 进入 h 返回";
|
|
1920
|
+
this.render();
|
|
1921
|
+
}
|
|
1922
|
+
openEmojiCategory(categoryId) {
|
|
1923
|
+
const category = EMOJI_CATEGORIES.find((item) => item.id === categoryId);
|
|
1924
|
+
if (!category) {
|
|
1925
|
+
this.state.status = "未找到表情分类";
|
|
1926
|
+
this.render();
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const items = category.codes.map((code) => {
|
|
1930
|
+
const art = getEmojiArt(code);
|
|
1931
|
+
return {
|
|
1932
|
+
title: `[${code}]`,
|
|
1933
|
+
meta: `emoji:${code}`,
|
|
1934
|
+
detail: art ? `${category.label} · ${art.width}x${art.height}px` : category.label
|
|
1935
|
+
};
|
|
1936
|
+
});
|
|
1937
|
+
this.openReadOnlyList(category.label, items, [
|
|
1938
|
+
{ title: "分类", detail: category.label },
|
|
1939
|
+
{ title: "数量", detail: `${category.codes.length} 个` },
|
|
1940
|
+
{ title: "来源", detail: `Assets/Emoji/${category.source}` }
|
|
1941
|
+
]);
|
|
1942
|
+
this.state.status = `${category.label}:j/k 选择 Enter 放大 h 返回分类`;
|
|
1943
|
+
this.render();
|
|
1944
|
+
}
|
|
1945
|
+
openEmojiDetail(code) {
|
|
1946
|
+
const art = getEmojiArt(code);
|
|
1947
|
+
const rendered = renderEmojiCode(code);
|
|
1948
|
+
if (!art || !rendered) {
|
|
1949
|
+
this.state.status = `未找到表情 [${code}]`;
|
|
1950
|
+
this.render();
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
this.state.modal = "info";
|
|
1954
|
+
this.state.infoTitle = `[${code}]`;
|
|
1955
|
+
this.state.infoLines = [
|
|
1956
|
+
...rendered.split("\n"),
|
|
1957
|
+
"",
|
|
1958
|
+
`尺寸: ${art.width}x${art.height}px`,
|
|
1959
|
+
`颜色: ${art.palette.length}`
|
|
1960
|
+
];
|
|
1961
|
+
this.render();
|
|
1962
|
+
}
|
|
1348
1963
|
async openAccountSwitcher() {
|
|
1349
1964
|
try {
|
|
1350
|
-
const accounts = await this.tokenStore.listAccounts();
|
|
1965
|
+
const accounts = (await this.tokenStore.listAccounts()).filter((account) => account.account !== "default");
|
|
1351
1966
|
const currentAccount = await this.tokenStore.getCurrentAccountName();
|
|
1352
1967
|
if (accounts.length === 0) {
|
|
1353
1968
|
this.state.modal = "info";
|
|
@@ -1366,7 +1981,7 @@ export class TuiController {
|
|
|
1366
1981
|
this.state.viewTitle = "切换账号";
|
|
1367
1982
|
this.state.items = items;
|
|
1368
1983
|
this.state.stats = [{ title: "账号数", detail: `${accounts.length}` }];
|
|
1369
|
-
this.state.itemIndex = accounts.findIndex(a => a.account === currentAccount);
|
|
1984
|
+
this.state.itemIndex = Math.max(0, accounts.findIndex(a => a.account === currentAccount));
|
|
1370
1985
|
this.state.scroll = 0;
|
|
1371
1986
|
this.state.focus = "content";
|
|
1372
1987
|
this.state.mode = "list";
|
|
@@ -1464,11 +2079,12 @@ export class TuiController {
|
|
|
1464
2079
|
const users = asArray(await this.client.getUsers(ids, false, signal));
|
|
1465
2080
|
this.openReadOnlyList(type === "follower" ? "粉丝列表" : "关注列表", users.map((user) => userItem(user)), [{ title: "用户", detail: `${users.length}` }]);
|
|
1466
2081
|
}
|
|
1467
|
-
openReadOnlyList(title, items, stats) {
|
|
2082
|
+
openReadOnlyList(title, items, stats, paging) {
|
|
1468
2083
|
this.snapshotParent();
|
|
1469
2084
|
this.state.viewTitle = title;
|
|
1470
2085
|
this.state.items = items;
|
|
1471
2086
|
this.state.stats = stats;
|
|
2087
|
+
this.state.listPaging = paging;
|
|
1472
2088
|
this.state.itemIndex = 0;
|
|
1473
2089
|
this.state.scroll = 0;
|
|
1474
2090
|
this.state.focus = "content";
|
|
@@ -1480,15 +2096,16 @@ export class TuiController {
|
|
|
1480
2096
|
this.render();
|
|
1481
2097
|
}
|
|
1482
2098
|
snapshotParent() {
|
|
1483
|
-
|
|
1484
|
-
this.state.
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2099
|
+
this.state.parentList = {
|
|
2100
|
+
title: this.state.viewTitle,
|
|
2101
|
+
items: [...this.state.items],
|
|
2102
|
+
stats: [...this.state.stats],
|
|
2103
|
+
itemIndex: this.state.itemIndex,
|
|
2104
|
+
scroll: this.state.scroll,
|
|
2105
|
+
status: this.state.status,
|
|
2106
|
+
paging: this.cloneListPaging(this.state.listPaging),
|
|
2107
|
+
parent: this.state.parentList
|
|
2108
|
+
};
|
|
1492
2109
|
}
|
|
1493
2110
|
restoreParentList() {
|
|
1494
2111
|
const parent = this.state.parentList;
|
|
@@ -1498,14 +2115,25 @@ export class TuiController {
|
|
|
1498
2115
|
this.state.items = parent.items;
|
|
1499
2116
|
this.state.stats = parent.stats;
|
|
1500
2117
|
this.state.itemIndex = parent.itemIndex;
|
|
2118
|
+
this.state.scroll = parent.scroll;
|
|
1501
2119
|
this.state.status = parent.status;
|
|
1502
|
-
this.state.
|
|
2120
|
+
this.state.listPaging = this.cloneListPaging(parent.paging);
|
|
2121
|
+
this.state.parentList = parent.parent;
|
|
1503
2122
|
this.state.currentBoard = undefined;
|
|
1504
2123
|
this.state.currentChat = undefined;
|
|
1505
2124
|
this.state.topic = undefined;
|
|
1506
2125
|
this.state.mode = "list";
|
|
1507
2126
|
this.state.focus = "content";
|
|
1508
|
-
this.
|
|
2127
|
+
this.render();
|
|
2128
|
+
}
|
|
2129
|
+
cloneListPaging(paging) {
|
|
2130
|
+
if (!paging)
|
|
2131
|
+
return undefined;
|
|
2132
|
+
return {
|
|
2133
|
+
...paging,
|
|
2134
|
+
boardCursors: paging.boardCursors?.map((cursor) => ({ ...cursor })),
|
|
2135
|
+
buffer: paging.buffer?.map((itemValue) => ({ ...itemValue }))
|
|
2136
|
+
};
|
|
1509
2137
|
}
|
|
1510
2138
|
async checkUpdate(forceShow = false) {
|
|
1511
2139
|
if (forceShow) {
|
|
@@ -1554,4 +2182,36 @@ export class TuiController {
|
|
|
1554
2182
|
}
|
|
1555
2183
|
}
|
|
1556
2184
|
}
|
|
2185
|
+
async function execFilePromise(command, args) {
|
|
2186
|
+
const { execFile } = await import("node:child_process");
|
|
2187
|
+
await new Promise((resolve, reject) => {
|
|
2188
|
+
execFile(command, args, (error) => {
|
|
2189
|
+
if (error) {
|
|
2190
|
+
reject(error);
|
|
2191
|
+
}
|
|
2192
|
+
else {
|
|
2193
|
+
resolve();
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
function appleScriptString(value) {
|
|
2199
|
+
return JSON.stringify(value);
|
|
2200
|
+
}
|
|
2201
|
+
function imageMimeType(path) {
|
|
2202
|
+
const lower = path.toLowerCase();
|
|
2203
|
+
if (lower.endsWith(".png"))
|
|
2204
|
+
return "image/png";
|
|
2205
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
2206
|
+
return "image/jpeg";
|
|
2207
|
+
if (lower.endsWith(".gif"))
|
|
2208
|
+
return "image/gif";
|
|
2209
|
+
if (lower.endsWith(".webp"))
|
|
2210
|
+
return "image/webp";
|
|
2211
|
+
if (lower.endsWith(".bmp"))
|
|
2212
|
+
return "image/bmp";
|
|
2213
|
+
if (lower.endsWith(".svg"))
|
|
2214
|
+
return "image/svg+xml";
|
|
2215
|
+
return "image/png";
|
|
2216
|
+
}
|
|
1557
2217
|
//# sourceMappingURL=controller.js.map
|