cc98-cli 0.4.2 → 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/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/components/content.d.ts +1 -0
- package/dist/tui/components/content.d.ts.map +1 -1
- package/dist/tui/components/content.js +21 -1
- package/dist/tui/components/content.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 +19 -1
- package/dist/tui/controller.d.ts.map +1 -1
- package/dist/tui/controller.js +341 -20
- 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 +3 -0
- package/dist/tui/navigation.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +145 -2
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/state/types.d.ts +8 -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.map +1 -1
- package/dist/tui/topic-reader.js +8 -4
- 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,8 +1,10 @@
|
|
|
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";
|
|
@@ -15,11 +17,13 @@ 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
|
+
constructor(state, client, tokenStore, render, close, nextSignal, abortCurrent, webVpnOptions) {
|
|
23
27
|
this.state = state;
|
|
24
28
|
this.client = client;
|
|
25
29
|
this.tokenStore = tokenStore;
|
|
@@ -27,10 +31,12 @@ export class TuiController {
|
|
|
27
31
|
this.close = close;
|
|
28
32
|
this.nextSignal = nextSignal;
|
|
29
33
|
this.abortCurrent = abortCurrent;
|
|
34
|
+
this.webVpnOptions = webVpnOptions;
|
|
30
35
|
this.keybindings = getKeybindingManager();
|
|
31
36
|
}
|
|
32
37
|
async load(force = false) {
|
|
33
38
|
const version = ++this.loadVersion;
|
|
39
|
+
let shouldAutoSignin = false;
|
|
34
40
|
const signal = this.nextSignal();
|
|
35
41
|
const nav = navItems[this.state.navIndex] ?? navItems[0];
|
|
36
42
|
this.state.viewTitle = nav.label;
|
|
@@ -58,6 +64,10 @@ export class TuiController {
|
|
|
58
64
|
this.updateChecked = true;
|
|
59
65
|
void this.checkUpdate();
|
|
60
66
|
}
|
|
67
|
+
if (!this.autoSigninChecked) {
|
|
68
|
+
this.autoSigninChecked = true;
|
|
69
|
+
shouldAutoSignin = true;
|
|
70
|
+
}
|
|
61
71
|
const next = await this.loadView(nav.id, force, signal);
|
|
62
72
|
if (version !== this.loadVersion)
|
|
63
73
|
return;
|
|
@@ -80,6 +90,9 @@ export class TuiController {
|
|
|
80
90
|
if (version === this.loadVersion) {
|
|
81
91
|
this.state.loading = false;
|
|
82
92
|
this.render();
|
|
93
|
+
if (shouldAutoSignin) {
|
|
94
|
+
void this.runAutoSignin();
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
}
|
|
@@ -420,11 +433,11 @@ export class TuiController {
|
|
|
420
433
|
this.openMenu();
|
|
421
434
|
}
|
|
422
435
|
}
|
|
423
|
-
// c
|
|
436
|
+
// c:图片复制图片本体,链接复制 URL
|
|
424
437
|
if (this.keybindings.matches(key, "topicCopyLink")) {
|
|
425
438
|
const currentLine = this.getCurrentTopicLine();
|
|
426
439
|
if (currentLine?.kind === "image" && currentLine.imageUrl) {
|
|
427
|
-
void this.
|
|
440
|
+
void this.copyImageToClipboard(currentLine.imageUrl);
|
|
428
441
|
}
|
|
429
442
|
else if (currentLine?.kind === "link" && currentLine.linkUrl) {
|
|
430
443
|
void this.copyToClipboard(currentLine.linkUrl);
|
|
@@ -432,8 +445,9 @@ export class TuiController {
|
|
|
432
445
|
}
|
|
433
446
|
}
|
|
434
447
|
handleSettingsKey(key) {
|
|
448
|
+
const itemCount = this.state.items.length || settingsItems.length;
|
|
435
449
|
if (this.keybindings.matches(key, "moveDown")) {
|
|
436
|
-
this.state.itemIndex = Math.min(
|
|
450
|
+
this.state.itemIndex = Math.min(itemCount - 1, this.state.itemIndex + 1);
|
|
437
451
|
this.render();
|
|
438
452
|
return;
|
|
439
453
|
}
|
|
@@ -450,7 +464,7 @@ export class TuiController {
|
|
|
450
464
|
return;
|
|
451
465
|
}
|
|
452
466
|
if (this.keybindings.matches(key, "confirm") || this.keybindings.matches(key, "moveRight")) {
|
|
453
|
-
void this.activateSetting(settingsItems[this.state.itemIndex]);
|
|
467
|
+
void this.activateSetting(this.state.items[this.state.itemIndex] ?? settingsItems[this.state.itemIndex]);
|
|
454
468
|
}
|
|
455
469
|
}
|
|
456
470
|
handleNavKey(key) {
|
|
@@ -705,11 +719,18 @@ export class TuiController {
|
|
|
705
719
|
};
|
|
706
720
|
}
|
|
707
721
|
case "settings": {
|
|
708
|
-
const cacheStats = await
|
|
722
|
+
const [cacheStats, autoSignin] = await Promise.all([
|
|
723
|
+
this.client.getCacheStats(),
|
|
724
|
+
this.settingsStore.isAutoSigninEnabled()
|
|
725
|
+
]);
|
|
709
726
|
return {
|
|
710
727
|
title: "设置",
|
|
711
|
-
items:
|
|
712
|
-
stats: [
|
|
728
|
+
items: this.renderSettingsItems(autoSignin),
|
|
729
|
+
stats: [
|
|
730
|
+
{ title: "自动签到", detail: autoSignin ? "已开启" : "已关闭" },
|
|
731
|
+
{ title: "缓存", detail: `${cacheStats.fileCacheEntries} 文件` },
|
|
732
|
+
{ title: "版本", detail: `v${appVersion}` }
|
|
733
|
+
],
|
|
713
734
|
status: "设置:j/k 选择 Enter 执行 h 返回"
|
|
714
735
|
};
|
|
715
736
|
}
|
|
@@ -731,6 +752,14 @@ export class TuiController {
|
|
|
731
752
|
void this.openCacheManager();
|
|
732
753
|
return;
|
|
733
754
|
}
|
|
755
|
+
if (selected.meta === "pixel-logo") {
|
|
756
|
+
this.openPixelLogo();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (selected.meta === "emoji-preview") {
|
|
760
|
+
this.openEmojiPreview();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
734
763
|
if (selected.meta === "update") {
|
|
735
764
|
void this.checkUpdate(true);
|
|
736
765
|
return;
|
|
@@ -739,6 +768,10 @@ export class TuiController {
|
|
|
739
768
|
void this.openAccountSwitcher();
|
|
740
769
|
return;
|
|
741
770
|
}
|
|
771
|
+
if (selected.meta === "auto-signin") {
|
|
772
|
+
void this.toggleAutoSignin();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
742
775
|
if (selected.meta === "logout") {
|
|
743
776
|
void this.confirmLogout();
|
|
744
777
|
return;
|
|
@@ -746,6 +779,64 @@ export class TuiController {
|
|
|
746
779
|
this.state.status = "功能开发中...";
|
|
747
780
|
this.render();
|
|
748
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) {
|
|
820
|
+
try {
|
|
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;
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
failed += 1;
|
|
831
|
+
}
|
|
832
|
+
this.state.status = `自动签到: ${success + failed}/${accounts.length}`;
|
|
833
|
+
this.render();
|
|
834
|
+
}
|
|
835
|
+
this.state.status = failed > 0
|
|
836
|
+
? `自动签到完成: ${success} 成功,${failed} 失败`
|
|
837
|
+
: `自动签到完成: ${success} 个账号`;
|
|
838
|
+
this.render();
|
|
839
|
+
}
|
|
749
840
|
async activateContentItem(selected, signal) {
|
|
750
841
|
if (selected.topicId !== undefined) {
|
|
751
842
|
await this.openTopic(selected.topicId, false, signal);
|
|
@@ -769,6 +860,19 @@ export class TuiController {
|
|
|
769
860
|
await this.switchAccount(accountName);
|
|
770
861
|
return;
|
|
771
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
|
+
}
|
|
772
876
|
if (selected.action?.startsWith("notices:")) {
|
|
773
877
|
await this.openNoticeList(selected.action.split(":")[1], signal);
|
|
774
878
|
return;
|
|
@@ -804,6 +908,10 @@ export class TuiController {
|
|
|
804
908
|
}
|
|
805
909
|
}
|
|
806
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
|
+
}
|
|
807
915
|
}
|
|
808
916
|
async openBoard(boardId, title, force, signal, pushParent = true) {
|
|
809
917
|
if (pushParent)
|
|
@@ -978,6 +1086,7 @@ export class TuiController {
|
|
|
978
1086
|
const posts = asArray(await this.client.getTopicPosts(topic.topicId, topic.loaded, topic.size, false, signal));
|
|
979
1087
|
appendTopicPosts(topic, posts);
|
|
980
1088
|
this.state.status = "";
|
|
1089
|
+
void this.preloadTopicImages(topic);
|
|
981
1090
|
}
|
|
982
1091
|
catch (error) {
|
|
983
1092
|
if (!isAbortError(error))
|
|
@@ -1257,6 +1366,54 @@ export class TuiController {
|
|
|
1257
1366
|
}
|
|
1258
1367
|
return undefined;
|
|
1259
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
|
+
}
|
|
1260
1417
|
async openImage(url) {
|
|
1261
1418
|
this.state.status = "正在下载图片...";
|
|
1262
1419
|
this.render();
|
|
@@ -1269,7 +1426,11 @@ export class TuiController {
|
|
|
1269
1426
|
const { execFile } = await import("node:child_process");
|
|
1270
1427
|
const platform = process.platform;
|
|
1271
1428
|
const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
1272
|
-
const args = platform === "win32"
|
|
1429
|
+
const args = platform === "win32"
|
|
1430
|
+
? ["/c", "start", "", localPath]
|
|
1431
|
+
: platform === "darwin"
|
|
1432
|
+
? ["-a", "Preview", localPath]
|
|
1433
|
+
: [localPath];
|
|
1273
1434
|
execFile(command, args, (error) => {
|
|
1274
1435
|
if (error) {
|
|
1275
1436
|
this.state.status = `打开失败: ${error.message}`;
|
|
@@ -1282,6 +1443,58 @@ export class TuiController {
|
|
|
1282
1443
|
this.render();
|
|
1283
1444
|
}
|
|
1284
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
|
+
}
|
|
1285
1498
|
async copyToClipboard(text) {
|
|
1286
1499
|
try {
|
|
1287
1500
|
const { spawn } = await import("node:child_process");
|
|
@@ -1345,6 +1558,81 @@ export class TuiController {
|
|
|
1345
1558
|
this.state.infoLines = lines;
|
|
1346
1559
|
this.render();
|
|
1347
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
|
+
}
|
|
1348
1636
|
async openAccountSwitcher() {
|
|
1349
1637
|
try {
|
|
1350
1638
|
const accounts = await this.tokenStore.listAccounts();
|
|
@@ -1480,15 +1768,15 @@ export class TuiController {
|
|
|
1480
1768
|
this.render();
|
|
1481
1769
|
}
|
|
1482
1770
|
snapshotParent() {
|
|
1483
|
-
|
|
1484
|
-
this.state.
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
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
|
+
};
|
|
1492
1780
|
}
|
|
1493
1781
|
restoreParentList() {
|
|
1494
1782
|
const parent = this.state.parentList;
|
|
@@ -1498,14 +1786,15 @@ export class TuiController {
|
|
|
1498
1786
|
this.state.items = parent.items;
|
|
1499
1787
|
this.state.stats = parent.stats;
|
|
1500
1788
|
this.state.itemIndex = parent.itemIndex;
|
|
1789
|
+
this.state.scroll = parent.scroll;
|
|
1501
1790
|
this.state.status = parent.status;
|
|
1502
|
-
this.state.parentList =
|
|
1791
|
+
this.state.parentList = parent.parent;
|
|
1503
1792
|
this.state.currentBoard = undefined;
|
|
1504
1793
|
this.state.currentChat = undefined;
|
|
1505
1794
|
this.state.topic = undefined;
|
|
1506
1795
|
this.state.mode = "list";
|
|
1507
1796
|
this.state.focus = "content";
|
|
1508
|
-
this.
|
|
1797
|
+
this.render();
|
|
1509
1798
|
}
|
|
1510
1799
|
async checkUpdate(forceShow = false) {
|
|
1511
1800
|
if (forceShow) {
|
|
@@ -1554,4 +1843,36 @@ export class TuiController {
|
|
|
1554
1843
|
}
|
|
1555
1844
|
}
|
|
1556
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";
|
|
1877
|
+
}
|
|
1557
1878
|
//# sourceMappingURL=controller.js.map
|