@walavave/cc98-cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +8 -2
  3. package/assets/forum-images/ac-dark/01.png +0 -0
  4. package/assets/forum-images/ac-dark/02.png +0 -0
  5. package/assets/forum-images/ac-dark/03.png +0 -0
  6. package/assets/forum-images/ac-dark/04.png +0 -0
  7. package/assets/forum-images/ac-dark/05.png +0 -0
  8. package/assets/forum-images/ac-dark/06.png +0 -0
  9. package/assets/forum-images/ac-dark/07.png +0 -0
  10. package/assets/forum-images/ac-dark/08.png +0 -0
  11. package/assets/forum-images/ac-dark/09.png +0 -0
  12. package/assets/forum-images/ac-dark/10.png +0 -0
  13. package/assets/forum-images/ac-dark/1001.png +0 -0
  14. package/assets/forum-images/ac-dark/1002.png +0 -0
  15. package/assets/forum-images/ac-dark/1003.png +0 -0
  16. package/assets/forum-images/ac-dark/1004.png +0 -0
  17. package/assets/forum-images/ac-dark/1005.png +0 -0
  18. package/assets/forum-images/ac-dark/1006.png +0 -0
  19. package/assets/forum-images/ac-dark/1007.png +0 -0
  20. package/assets/forum-images/ac-dark/1008.png +0 -0
  21. package/assets/forum-images/ac-dark/1009.png +0 -0
  22. package/assets/forum-images/ac-dark/1010.png +0 -0
  23. package/assets/forum-images/ac-dark/1011.png +0 -0
  24. package/assets/forum-images/ac-dark/1012.png +0 -0
  25. package/assets/forum-images/ac-dark/1013.png +0 -0
  26. package/assets/forum-images/ac-dark/1014.png +0 -0
  27. package/assets/forum-images/ac-dark/1015.png +0 -0
  28. package/assets/forum-images/ac-dark/1016.png +0 -0
  29. package/assets/forum-images/ac-dark/1017.png +0 -0
  30. package/assets/forum-images/ac-dark/1018.png +0 -0
  31. package/assets/forum-images/ac-dark/1019.png +0 -0
  32. package/assets/forum-images/ac-dark/1020.png +0 -0
  33. package/assets/forum-images/ac-dark/1021.png +0 -0
  34. package/assets/forum-images/ac-dark/1022.png +0 -0
  35. package/assets/forum-images/ac-dark/1023.png +0 -0
  36. package/assets/forum-images/ac-dark/1024.png +0 -0
  37. package/assets/forum-images/ac-dark/1025.png +0 -0
  38. package/assets/forum-images/ac-dark/1026.png +0 -0
  39. package/assets/forum-images/ac-dark/1027.png +0 -0
  40. package/assets/forum-images/ac-dark/1028.png +0 -0
  41. package/assets/forum-images/ac-dark/1029.png +0 -0
  42. package/assets/forum-images/ac-dark/1030.png +0 -0
  43. package/assets/forum-images/ac-dark/1031.png +0 -0
  44. package/assets/forum-images/ac-dark/1032.png +0 -0
  45. package/assets/forum-images/ac-dark/1033.png +0 -0
  46. package/assets/forum-images/ac-dark/1034.png +0 -0
  47. package/assets/forum-images/ac-dark/1035.png +0 -0
  48. package/assets/forum-images/ac-dark/1036.png +0 -0
  49. package/assets/forum-images/ac-dark/1037.png +0 -0
  50. package/assets/forum-images/ac-dark/1038.png +0 -0
  51. package/assets/forum-images/ac-dark/1039.png +0 -0
  52. package/assets/forum-images/ac-dark/1040.png +0 -0
  53. package/assets/forum-images/ac-dark/11.png +0 -0
  54. package/assets/forum-images/ac-dark/12.png +0 -0
  55. package/assets/forum-images/ac-dark/13.png +0 -0
  56. package/assets/forum-images/ac-dark/14.png +0 -0
  57. package/assets/forum-images/ac-dark/15.png +0 -0
  58. package/assets/forum-images/ac-dark/16.png +0 -0
  59. package/assets/forum-images/ac-dark/17.png +0 -0
  60. package/assets/forum-images/ac-dark/18.png +0 -0
  61. package/assets/forum-images/ac-dark/19.png +0 -0
  62. package/assets/forum-images/ac-dark/20.png +0 -0
  63. package/assets/forum-images/ac-dark/2001.png +0 -0
  64. package/assets/forum-images/ac-dark/2002.png +0 -0
  65. package/assets/forum-images/ac-dark/2003.png +0 -0
  66. package/assets/forum-images/ac-dark/2004.png +0 -0
  67. package/assets/forum-images/ac-dark/2005.png +0 -0
  68. package/assets/forum-images/ac-dark/2006.png +0 -0
  69. package/assets/forum-images/ac-dark/2007.png +0 -0
  70. package/assets/forum-images/ac-dark/2008.png +0 -0
  71. package/assets/forum-images/ac-dark/2009.png +0 -0
  72. package/assets/forum-images/ac-dark/2010.png +0 -0
  73. package/assets/forum-images/ac-dark/2011.png +0 -0
  74. package/assets/forum-images/ac-dark/2012.png +0 -0
  75. package/assets/forum-images/ac-dark/2013.png +0 -0
  76. package/assets/forum-images/ac-dark/2014.png +0 -0
  77. package/assets/forum-images/ac-dark/2015.png +0 -0
  78. package/assets/forum-images/ac-dark/2016.png +0 -0
  79. package/assets/forum-images/ac-dark/2017.png +0 -0
  80. package/assets/forum-images/ac-dark/2018.png +0 -0
  81. package/assets/forum-images/ac-dark/2019.png +0 -0
  82. package/assets/forum-images/ac-dark/2020.png +0 -0
  83. package/assets/forum-images/ac-dark/2021.png +0 -0
  84. package/assets/forum-images/ac-dark/2022.png +0 -0
  85. package/assets/forum-images/ac-dark/2023.png +0 -0
  86. package/assets/forum-images/ac-dark/2024.png +0 -0
  87. package/assets/forum-images/ac-dark/2025.png +0 -0
  88. package/assets/forum-images/ac-dark/2026.png +0 -0
  89. package/assets/forum-images/ac-dark/2027.png +0 -0
  90. package/assets/forum-images/ac-dark/2028.png +0 -0
  91. package/assets/forum-images/ac-dark/2029.png +0 -0
  92. package/assets/forum-images/ac-dark/2030.png +0 -0
  93. package/assets/forum-images/ac-dark/2031.png +0 -0
  94. package/assets/forum-images/ac-dark/2032.png +0 -0
  95. package/assets/forum-images/ac-dark/2033.png +0 -0
  96. package/assets/forum-images/ac-dark/2034.png +0 -0
  97. package/assets/forum-images/ac-dark/2035.png +0 -0
  98. package/assets/forum-images/ac-dark/2036.png +0 -0
  99. package/assets/forum-images/ac-dark/2037.png +0 -0
  100. package/assets/forum-images/ac-dark/2038.png +0 -0
  101. package/assets/forum-images/ac-dark/2039.png +0 -0
  102. package/assets/forum-images/ac-dark/2040.png +0 -0
  103. package/assets/forum-images/ac-dark/2041.png +0 -0
  104. package/assets/forum-images/ac-dark/2042.png +0 -0
  105. package/assets/forum-images/ac-dark/2043.png +0 -0
  106. package/assets/forum-images/ac-dark/2044.png +0 -0
  107. package/assets/forum-images/ac-dark/2045.png +0 -0
  108. package/assets/forum-images/ac-dark/2046.png +0 -0
  109. package/assets/forum-images/ac-dark/2047.png +0 -0
  110. package/assets/forum-images/ac-dark/2048.png +0 -0
  111. package/assets/forum-images/ac-dark/2049.png +0 -0
  112. package/assets/forum-images/ac-dark/2050.png +0 -0
  113. package/assets/forum-images/ac-dark/2051.png +0 -0
  114. package/assets/forum-images/ac-dark/2052.png +0 -0
  115. package/assets/forum-images/ac-dark/2053.png +0 -0
  116. package/assets/forum-images/ac-dark/2054.png +0 -0
  117. package/assets/forum-images/ac-dark/2055.png +0 -0
  118. package/assets/forum-images/ac-dark/21.png +0 -0
  119. package/assets/forum-images/ac-dark/22.png +0 -0
  120. package/assets/forum-images/ac-dark/23.png +0 -0
  121. package/assets/forum-images/ac-dark/24.png +0 -0
  122. package/assets/forum-images/ac-dark/25.png +0 -0
  123. package/assets/forum-images/ac-dark/26.png +0 -0
  124. package/assets/forum-images/ac-dark/27.png +0 -0
  125. package/assets/forum-images/ac-dark/28.png +0 -0
  126. package/assets/forum-images/ac-dark/29.png +0 -0
  127. package/assets/forum-images/ac-dark/30.png +0 -0
  128. package/assets/forum-images/ac-dark/31.png +0 -0
  129. package/assets/forum-images/ac-dark/32.png +0 -0
  130. package/assets/forum-images/ac-dark/33.png +0 -0
  131. package/assets/forum-images/ac-dark/34.png +0 -0
  132. package/assets/forum-images/ac-dark/35.png +0 -0
  133. package/assets/forum-images/ac-dark/36.png +0 -0
  134. package/assets/forum-images/ac-dark/37.png +0 -0
  135. package/assets/forum-images/ac-dark/38.png +0 -0
  136. package/assets/forum-images/ac-dark/39.png +0 -0
  137. package/assets/forum-images/ac-dark/40.png +0 -0
  138. package/assets/forum-images/ac-dark/41.png +0 -0
  139. package/assets/forum-images/ac-dark/42.png +0 -0
  140. package/assets/forum-images/ac-dark/43.png +0 -0
  141. package/assets/forum-images/ac-dark/44.png +0 -0
  142. package/assets/forum-images/ac-dark/45.png +0 -0
  143. package/assets/forum-images/ac-dark/46.png +0 -0
  144. package/assets/forum-images/ac-dark/47.png +0 -0
  145. package/assets/forum-images/ac-dark/48.png +0 -0
  146. package/assets/forum-images/ac-dark/49.png +0 -0
  147. package/assets/forum-images/ac-dark/50.png +0 -0
  148. package/assets/forum-images/ac-dark/51.png +0 -0
  149. package/assets/forum-images/ac-dark/52.png +0 -0
  150. package/assets/forum-images/ac-dark/53.png +0 -0
  151. package/assets/forum-images/ac-dark/54.png +0 -0
  152. package/dist/api/client.d.ts +1 -0
  153. package/dist/api/client.d.ts.map +1 -1
  154. package/dist/api/client.js +17 -0
  155. package/dist/api/client.js.map +1 -1
  156. package/dist/api/endpoints.d.ts +1 -0
  157. package/dist/api/endpoints.d.ts.map +1 -1
  158. package/dist/api/endpoints.js +1 -0
  159. package/dist/api/endpoints.js.map +1 -1
  160. package/dist/config.d.ts +2 -0
  161. package/dist/config.d.ts.map +1 -1
  162. package/dist/config.js +25 -4
  163. package/dist/config.js.map +1 -1
  164. package/dist/tui/app-runtime/content.d.ts +9 -0
  165. package/dist/tui/app-runtime/content.d.ts.map +1 -0
  166. package/dist/tui/app-runtime/content.js +275 -0
  167. package/dist/tui/app-runtime/content.js.map +1 -0
  168. package/dist/tui/app-runtime/context.d.ts +24 -0
  169. package/dist/tui/app-runtime/context.d.ts.map +1 -0
  170. package/dist/tui/app-runtime/context.js +2 -0
  171. package/dist/tui/app-runtime/context.js.map +1 -0
  172. package/dist/tui/app-runtime/image-viewer.d.ts +5 -0
  173. package/dist/tui/app-runtime/image-viewer.d.ts.map +1 -0
  174. package/dist/tui/app-runtime/image-viewer.js +112 -0
  175. package/dist/tui/app-runtime/image-viewer.js.map +1 -0
  176. package/dist/tui/app-runtime/interactions.d.ts +6 -0
  177. package/dist/tui/app-runtime/interactions.d.ts.map +1 -0
  178. package/dist/tui/app-runtime/interactions.js +181 -0
  179. package/dist/tui/app-runtime/interactions.js.map +1 -0
  180. package/dist/tui/app-runtime/keyboard.d.ts +3 -0
  181. package/dist/tui/app-runtime/keyboard.d.ts.map +1 -0
  182. package/dist/tui/app-runtime/keyboard.js +284 -0
  183. package/dist/tui/app-runtime/keyboard.js.map +1 -0
  184. package/dist/tui/app-runtime/modals.d.ts +17 -0
  185. package/dist/tui/app-runtime/modals.d.ts.map +1 -0
  186. package/dist/tui/app-runtime/modals.js +343 -0
  187. package/dist/tui/app-runtime/modals.js.map +1 -0
  188. package/dist/tui/app-runtime/mouse.d.ts +5 -0
  189. package/dist/tui/app-runtime/mouse.d.ts.map +1 -0
  190. package/dist/tui/app-runtime/mouse.js +68 -0
  191. package/dist/tui/app-runtime/mouse.js.map +1 -0
  192. package/dist/tui/app-runtime/state.d.ts +6 -0
  193. package/dist/tui/app-runtime/state.d.ts.map +1 -0
  194. package/dist/tui/app-runtime/state.js +41 -0
  195. package/dist/tui/app-runtime/state.js.map +1 -0
  196. package/dist/tui/app-runtime/topic.d.ts +6 -0
  197. package/dist/tui/app-runtime/topic.d.ts.map +1 -0
  198. package/dist/tui/app-runtime/topic.js +184 -0
  199. package/dist/tui/app-runtime/topic.js.map +1 -0
  200. package/dist/tui/app-runtime.d.ts +2 -27
  201. package/dist/tui/app-runtime.d.ts.map +1 -1
  202. package/dist/tui/app-runtime.js +2 -1132
  203. package/dist/tui/app-runtime.js.map +1 -1
  204. package/dist/tui/app.d.ts.map +1 -1
  205. package/dist/tui/app.js +2 -1
  206. package/dist/tui/app.js.map +1 -1
  207. package/dist/tui/cached-client.d.ts +1 -0
  208. package/dist/tui/cached-client.d.ts.map +1 -1
  209. package/dist/tui/cached-client.js +3 -0
  210. package/dist/tui/cached-client.js.map +1 -1
  211. package/dist/tui/emotion-catalog.d.ts +23 -0
  212. package/dist/tui/emotion-catalog.d.ts.map +1 -0
  213. package/dist/tui/emotion-catalog.js +150 -0
  214. package/dist/tui/emotion-catalog.js.map +1 -0
  215. package/dist/tui/emotion-preview.js +1 -1
  216. package/dist/tui/emotion-preview.js.map +1 -1
  217. package/dist/tui/image-preview.d.ts.map +1 -1
  218. package/dist/tui/image-preview.js +2 -1
  219. package/dist/tui/image-preview.js.map +1 -1
  220. package/dist/tui/keymap.d.ts +1 -1
  221. package/dist/tui/keymap.d.ts.map +1 -1
  222. package/dist/tui/keymap.js +2 -0
  223. package/dist/tui/keymap.js.map +1 -1
  224. package/dist/tui/renderer.d.ts.map +1 -1
  225. package/dist/tui/renderer.js +212 -3
  226. package/dist/tui/renderer.js.map +1 -1
  227. package/dist/tui/terminal.d.ts +3 -0
  228. package/dist/tui/terminal.d.ts.map +1 -1
  229. package/dist/tui/terminal.js +37 -0
  230. package/dist/tui/terminal.js.map +1 -1
  231. package/dist/tui/theme.d.ts +1 -0
  232. package/dist/tui/theme.d.ts.map +1 -1
  233. package/dist/tui/theme.js +1 -0
  234. package/dist/tui/theme.js.map +1 -1
  235. package/dist/tui/tui-model.d.ts +11 -1
  236. package/dist/tui/tui-model.d.ts.map +1 -1
  237. package/dist/tui/tui-model.js.map +1 -1
  238. package/dist/tui/ubb-renderer.js +1 -1
  239. package/dist/tui/ubb-renderer.js.map +1 -1
  240. package/dist/version.d.ts +1 -1
  241. package/dist/version.js +1 -1
  242. package/package.json +1 -1
@@ -1,1133 +1,3 @@
1
- import { checkForUpdate } from "../update.js";
2
- import { createLoginForm, isPrintableInput, updateLoginField } from "./account-modal.js";
3
- import { executeSearch, getDefaultAccountName, jumpRelativeTopicFloor, jumpToTopicFloor, loadNextChatPage, loadNextSearchPage, loadNextTopicPage, normalizeLoginMessage, openBoard, openChat, openTopic, refreshAccounts, restoreParentList } from "./app-data.js";
4
- import { isEmotionAssetPath } from "./emotion-preview.js";
5
- import { loadModalImagePreview, supportsImagePreview } from "./image-preview.js";
6
- import { fill, length, pad, rect, split } from "./layout.js";
7
- import { getSidebarWidth } from "./renderer.js";
8
- import { downloadUrlToDownloads } from "./downloads.js";
9
- import { currentTopicLine, currentTopicPost, getStatus, navItems, settingsItems } from "./tui-model.js";
10
- export function createMouseHandler(context, handleScroll, clampSidebarWidth, getDividerColumn) {
11
- let pendingScrollRender;
12
- const scheduleScrollRender = () => {
13
- if (pendingScrollRender) {
14
- clearTimeout(pendingScrollRender);
15
- }
16
- pendingScrollRender = setTimeout(() => {
17
- pendingScrollRender = undefined;
18
- context.render();
19
- }, 80);
20
- };
21
- return (event) => {
22
- const { state, render } = context;
23
- if (state.modal) {
24
- if (event.kind === "up") {
25
- state.draggingSidebarDivider = false;
26
- }
27
- return;
28
- }
29
- const size = context.getSize();
30
- const dividerColumn = getDividerColumn();
31
- const withinFrame = event.row >= 2 && event.row < size.rows - 1;
32
- if (event.kind === "down" && event.button === "wheel-up") {
33
- handleScroll(state, -3);
34
- scheduleScrollRender();
35
- return;
36
- }
37
- if (event.kind === "down" && event.button === "wheel-down") {
38
- const wasAtTopicEnd = isAtTopicEnd(state, context.config, size.rows);
39
- const wasAtSearchEnd = isAtSearchEnd(state);
40
- handleScroll(state, 3);
41
- scheduleScrollRender();
42
- if (wasAtTopicEnd && state.mode === "topic" && state.topic?.hasMore && !state.loadingMore) {
43
- void loadNextTopicPage(context.client, state, render, context.config, context.nextSignal(), true);
44
- }
45
- else if (wasAtSearchEnd && state.currentSearch?.hasMore && !state.loadingMore && !state.loading) {
46
- void loadNextSearchPage(context.client, state, render, context.nextSignal());
47
- }
48
- return;
49
- }
50
- if (event.kind === "down" && event.button === "left" && withinFrame && event.column === dividerColumn) {
51
- state.draggingSidebarDivider = true;
52
- return;
53
- }
54
- if (event.kind === "drag" && state.draggingSidebarDivider) {
55
- state.sidebarWidth = clampSidebarWidth(event.column - 1, size.columns);
56
- render();
57
- return;
58
- }
59
- if (event.kind === "up") {
60
- state.draggingSidebarDivider = false;
61
- if (event.button === "left") {
62
- if (handleSidebarClick(context, event, size.columns, size.rows)) {
63
- return;
64
- }
65
- if (handleContentClick(context, event, size.columns, size.rows)) {
66
- return;
67
- }
68
- void handleTopicClick(context, event, size.columns, size.rows);
69
- }
70
- }
71
- };
72
- }
73
- export function createKeyHandler(context) {
74
- return (key) => {
75
- const { keymap, state, close, render } = context;
76
- const keyAction = keymap.feed(key);
77
- if (key === "\u0003" || key === "q") {
78
- close();
79
- return;
80
- }
81
- if (key === "?") {
82
- state.modal = state.modal === "help" ? null : "help";
83
- render();
84
- return;
85
- }
86
- if (state.modal === "help") {
87
- if (key === "h" || key === "\x1b[D" || key === "\x1b" || key === "?" || key === "\r") {
88
- state.modal = null;
89
- render();
90
- }
91
- return;
92
- }
93
- if (state.modal === "account") {
94
- handleAccountModal(context, key);
95
- return;
96
- }
97
- if (state.modal === "login") {
98
- handleLoginModal(context, key);
99
- return;
100
- }
101
- if (state.modal === "confirm") {
102
- handleConfirmModal(context, key);
103
- return;
104
- }
105
- if (state.modal === "image") {
106
- handleImageModal(context, key);
107
- return;
108
- }
109
- if (keyAction === "search.focus-input") {
110
- void focusSearchInput(context);
111
- return;
112
- }
113
- if (state.mode === "topic") {
114
- handleTopicMode(context, key, keyAction);
115
- return;
116
- }
117
- if (state.mode === "settings") {
118
- handleSettingsMode(context, key);
119
- return;
120
- }
121
- if (state.focus === "nav") {
122
- handleNavFocus(context, key);
123
- return;
124
- }
125
- handleContentFocus(context, key);
126
- };
127
- }
128
- async function focusSearchInput(context) {
129
- const { state, render, load, abortCurrent } = context;
130
- const searchNavIndex = navItems.findIndex((item) => item.id === "search");
131
- if (searchNavIndex < 0) {
132
- return;
133
- }
134
- if (state.navIndex === searchNavIndex && state.currentSearch) {
135
- abortCurrent();
136
- state.mode = "list";
137
- state.focus = "content";
138
- state.loading = false;
139
- state.loadingMore = false;
140
- state.error = undefined;
141
- state.topic = undefined;
142
- state.imageViewer = undefined;
143
- state.currentSearch.focus = "input";
144
- state.viewTitle = state.currentSearch.title;
145
- state.status = getStatus(state);
146
- render();
147
- return;
148
- }
149
- abortCurrent();
150
- state.navIndex = searchNavIndex;
151
- await load();
152
- if (navItems[state.navIndex]?.id !== "search" || !state.currentSearch) {
153
- return;
154
- }
155
- state.mode = "list";
156
- state.focus = "content";
157
- state.currentSearch.focus = "input";
158
- state.status = getStatus(state);
159
- render();
160
- }
161
- function showNotification(state, message, durationMs = 3200) {
162
- state.notification = {
163
- message,
164
- expiresAt: Date.now() + durationMs
165
- };
166
- }
167
- function handleAccountModal(context, key) {
168
- const { state, render, tokenStore, load } = context;
169
- if (key === "j" || key === "\x1b[B") {
170
- state.accountModal.selectedIndex = Math.min(state.accountModal.accounts.length, state.accountModal.selectedIndex + 1);
171
- render();
172
- return;
173
- }
174
- if (key === "k" || key === "\x1b[A") {
175
- state.accountModal.selectedIndex = Math.max(0, state.accountModal.selectedIndex - 1);
176
- render();
177
- return;
178
- }
179
- if (key === "h" || key === "\x1b[D" || key === "\x1b") {
180
- state.modal = null;
181
- state.status = getStatus(state);
182
- render();
183
- return;
184
- }
185
- if (key === "\r" || key === "l" || key === "\x1b[C") {
186
- if (state.accountModal.selectedIndex === state.accountModal.accounts.length) {
187
- state.loginForm = createLoginForm();
188
- state.modal = "login";
189
- render();
190
- return;
191
- }
192
- const selected = state.accountModal.accounts[state.accountModal.selectedIndex];
193
- if (!selected) {
194
- return;
195
- }
196
- state.status = `正在切换到 @${selected.account}...`;
197
- state.modal = null;
198
- render();
199
- void tokenStore.useAccount(selected.account).then(() => {
200
- state.account = selected.account;
201
- showNotification(state, `已切换到 @${selected.account}`);
202
- void load(true);
203
- }).catch((error) => {
204
- state.error = error instanceof Error ? error.message : String(error);
205
- state.status = "账号切换失败";
206
- render();
207
- });
208
- }
209
- }
210
- function handleLoginModal(context, key) {
211
- const { state, render, rawClient, tokenStore, load } = context;
212
- if (state.loginForm.submitting) {
213
- return;
214
- }
215
- if (key === "\t" || key === "j" || key === "\x1b[B") {
216
- state.loginForm.fieldIndex = (state.loginForm.fieldIndex + 1) % 3;
217
- render();
218
- return;
219
- }
220
- if (key === "k" || key === "\x1b[A") {
221
- state.loginForm.fieldIndex = (state.loginForm.fieldIndex + 2) % 3;
222
- render();
223
- return;
224
- }
225
- if (key === "h" || key === "\x1b[D" || key === "\x1b") {
226
- state.modal = "account";
227
- state.loginForm.error = undefined;
228
- state.status = getStatus(state);
229
- render();
230
- return;
231
- }
232
- if (key === "l" || key === "\x1b[C") {
233
- if (state.loginForm.fieldIndex < 2) {
234
- state.loginForm.fieldIndex += 1;
235
- render();
236
- }
237
- return;
238
- }
239
- if (key === "\x7f") {
240
- updateLoginField(state.loginForm, (value) => value.slice(0, -1));
241
- render();
242
- return;
243
- }
244
- if (key === "\r") {
245
- if (state.loginForm.fieldIndex < 2) {
246
- state.loginForm.fieldIndex += 1;
247
- render();
248
- return;
249
- }
250
- const username = state.loginForm.username.trim();
251
- const password = state.loginForm.password;
252
- if (!username || !password) {
253
- state.loginForm.error = "用户名和密码不能为空";
254
- render();
255
- return;
256
- }
257
- state.loginForm.submitting = true;
258
- state.loginForm.error = undefined;
259
- state.status = `正在登录 ${username}...`;
260
- render();
261
- void rawClient.loginWithPassword(username, password).then(async (token) => {
262
- const me = await rawClient.getMeWithAccessToken(token.accessToken);
263
- const resolvedAccount = getDefaultAccountName(me, username);
264
- await tokenStore.saveAccount(resolvedAccount, {
265
- accessToken: token.accessToken,
266
- refreshToken: token.refreshToken,
267
- userId: typeof me.id === "number" ? me.id : undefined,
268
- username,
269
- displayName: typeof me.name === "string" ? me.name : undefined
270
- });
271
- state.account = resolvedAccount;
272
- await refreshAccounts(state, tokenStore);
273
- state.loginForm = createLoginForm();
274
- state.modal = null;
275
- showNotification(state, `已登录为 ${typeof me.name === "string" ? me.name : username}`);
276
- await load(true);
277
- }).catch((error) => {
278
- state.loginForm.submitting = false;
279
- state.loginForm.error = normalizeLoginMessage(error);
280
- state.status = "登录失败";
281
- render();
282
- });
283
- return;
284
- }
285
- if (isPrintableInput(key)) {
286
- updateLoginField(state.loginForm, (value) => `${value}${key}`);
287
- render();
288
- }
289
- }
290
- function handleConfirmModal(context, key) {
291
- const { state, render, client, tokenStore, load } = context;
292
- if (!state.confirmDialog) {
293
- state.modal = null;
294
- render();
295
- return;
296
- }
297
- if (key === "j" || key === "\x1b[B" || key === "\t") {
298
- state.confirmDialog.selectedIndex = Math.min(1, state.confirmDialog.selectedIndex + 1);
299
- render();
300
- return;
301
- }
302
- if (key === "k" || key === "\x1b[A") {
303
- state.confirmDialog.selectedIndex = Math.max(0, state.confirmDialog.selectedIndex - 1);
304
- render();
305
- return;
306
- }
307
- if (key === "h" || key === "\x1b[D" || key === "\x1b") {
308
- state.modal = null;
309
- state.confirmDialog = undefined;
310
- state.status = getStatus(state);
311
- render();
312
- return;
313
- }
314
- if (key !== "\r") {
315
- return;
316
- }
317
- if (state.confirmDialog.selectedIndex === 1) {
318
- state.modal = null;
319
- state.confirmDialog = undefined;
320
- state.status = getStatus(state);
321
- render();
322
- return;
323
- }
324
- const action = state.confirmDialog.action;
325
- state.modal = null;
326
- if (action === "cache-cleanup") {
327
- state.status = "正在清理缓存...";
328
- render();
329
- void client.clearCache().then(() => {
330
- showNotification(state, "缓存已清理");
331
- void load(true);
332
- }).catch((error) => {
333
- state.error = error instanceof Error ? error.message : String(error);
334
- state.status = "缓存清理失败";
335
- render();
336
- }).finally(() => {
337
- state.confirmDialog = undefined;
338
- });
339
- return;
340
- }
341
- const account = state.account;
342
- state.status = account ? `正在退出 @${account}...` : "正在清除登录信息...";
343
- render();
344
- void (async () => {
345
- if (account) {
346
- await tokenStore.removeAccount(account);
347
- }
348
- else {
349
- await tokenStore.clear();
350
- }
351
- state.account = await tokenStore.getCurrentAccountName();
352
- await refreshAccounts(state, tokenStore);
353
- showNotification(state, "已退出登录");
354
- await load(true);
355
- })().catch((error) => {
356
- state.error = error instanceof Error ? error.message : String(error);
357
- state.status = "退出登录失败";
358
- render();
359
- }).finally(() => {
360
- state.confirmDialog = undefined;
361
- });
362
- }
363
- function handleTopicMode(context, key, keyAction) {
364
- const { state, render, client, config, nextSignal, abortCurrent } = context;
365
- if (key === ":" && state.topic && !state.topic.floorInput) {
366
- state.topic.floorInput = ":";
367
- state.status = "跳转到楼层:输入数字后 Enter 确认 Esc 取消";
368
- render();
369
- return;
370
- }
371
- if (/^\d$/.test(key) && state.topic?.floorInput.startsWith(":")) {
372
- if (state.topic.floorInput === ":" && key === "0") {
373
- return;
374
- }
375
- state.topic.floorInput = `${state.topic.floorInput}${key}`.slice(0, 7);
376
- state.status = `跳转到 ${state.topic.floorInput.slice(1)} 楼:Enter 确认 Esc 取消`;
377
- render();
378
- return;
379
- }
380
- if (key === "\x7f" && state.topic?.floorInput) {
381
- state.topic.floorInput = state.topic.floorInput.slice(0, -1);
382
- state.status = state.topic.floorInput
383
- ? state.topic.floorInput === ":"
384
- ? "跳转到楼层:输入数字后 Enter 确认 Esc 取消"
385
- : `跳转到 ${state.topic.floorInput.slice(1)} 楼:Enter 确认 Esc 取消`
386
- : getStatus(state);
387
- render();
388
- return;
389
- }
390
- if (key === "\r" && state.topic?.floorInput) {
391
- const floor = Number(state.topic.floorInput.slice(1));
392
- state.topic.floorInput = "";
393
- if (Number.isInteger(floor) && floor > 0) {
394
- void jumpToTopicFloor(client, state, floor, render, config, nextSignal());
395
- }
396
- else {
397
- state.status = getStatus(state);
398
- render();
399
- }
400
- return;
401
- }
402
- if ((key === "]" || key === "】" || keyAction === "topic.next-reply") && state.topic) {
403
- jumpRelativeTopicFloor(state, 1);
404
- state.status = getStatus(state);
405
- render();
406
- return;
407
- }
408
- if ((key === "[" || key === "【" || keyAction === "topic.previous-reply") && state.topic) {
409
- jumpRelativeTopicFloor(state, -1);
410
- state.status = getStatus(state);
411
- render();
412
- return;
413
- }
414
- if (key === "a" || keyAction === "topic.like-post") {
415
- void reactToCurrentTopicPost(context, true);
416
- return;
417
- }
418
- if (key === "s" || keyAction === "topic.dislike-post") {
419
- void reactToCurrentTopicPost(context, false);
420
- return;
421
- }
422
- if (key === "h" || key === "\x1b[D") {
423
- abortCurrent();
424
- leaveTopicMode(state);
425
- render();
426
- return;
427
- }
428
- if (key === "\x1b" && state.topic?.floorInput) {
429
- state.topic.floorInput = "";
430
- state.status = getStatus(state);
431
- render();
432
- return;
433
- }
434
- if (key === "j" || key === "\x1b[B") {
435
- const maxScroll = Math.max(0, (state.topic?.lines.length ?? 0) - 1);
436
- const wasAtEnd = isAtTopicEnd(state, config, context.getSize().rows);
437
- state.scroll = Math.min(maxScroll, state.scroll + 1);
438
- state.status = getStatus(state);
439
- render();
440
- if (wasAtEnd && state.topic?.hasMore && !state.loadingMore) {
441
- void loadNextTopicPage(client, state, render, config, nextSignal(), true);
442
- }
443
- return;
444
- }
445
- if (key === "k" || key === "\x1b[A") {
446
- state.scroll = Math.max(0, state.scroll - 1);
447
- state.status = getStatus(state);
448
- render();
449
- return;
450
- }
451
- if (key === " ") {
452
- void openTopicImageViewer(context);
453
- return;
454
- }
455
- if (key === "n") {
456
- void loadNextTopicPage(client, state, render, config, nextSignal());
457
- return;
458
- }
459
- if (key === "\x1b[C") {
460
- void stepTopicImageViewer(context, 1);
461
- return;
462
- }
463
- if (key === "r") {
464
- if (state.topic) {
465
- void openTopic(client, state, state.topic.topicId, render, config, true, nextSignal());
466
- }
467
- return;
468
- }
469
- }
470
- function isAtTopicEnd(state, config, totalRows) {
471
- if (!state.topic) {
472
- return false;
473
- }
474
- const viewport = getTopicViewportHeight(config, totalRows);
475
- if (viewport <= 0) {
476
- return state.scroll >= Math.max(0, state.topic.lines.length - 1);
477
- }
478
- return state.scroll + viewport >= state.topic.lines.length;
479
- }
480
- function isAtSearchEnd(state) {
481
- return Boolean(state.currentSearch &&
482
- state.currentSearch.focus === "results" &&
483
- state.items.length > 0 &&
484
- state.itemIndex >= state.items.length - 1);
485
- }
486
- function getTopicViewportHeight(config, totalRows) {
487
- const mainHeight = config.hideTopChrome
488
- ? Math.max(1, totalRows - 3)
489
- : Math.max(1, totalRows - 7);
490
- return Math.max(0, mainHeight - 4);
491
- }
492
- async function reactToCurrentTopicPost(context, isLike) {
493
- const { state, client, render } = context;
494
- const topic = state.topic;
495
- if (!topic || state.loadingMore) {
496
- return;
497
- }
498
- const post = currentTopicPost(topic, state.scroll);
499
- if (!post?.id) {
500
- state.status = "当前楼层不可赞踩";
501
- render();
502
- return;
503
- }
504
- state.status = isLike ? "正在点赞..." : "正在点踩...";
505
- render();
506
- try {
507
- await client.reactToPost(post.id, isLike);
508
- const latest = await client.getPostReactionState(post.id, true);
509
- if (typeof latest === "object" && latest !== null) {
510
- const reaction = latest;
511
- if (typeof reaction.likeCount === "number" && Number.isFinite(reaction.likeCount)) {
512
- post.likeCount = reaction.likeCount;
513
- }
514
- if (typeof reaction.dislikeCount === "number" && Number.isFinite(reaction.dislikeCount)) {
515
- post.dislikeCount = reaction.dislikeCount;
516
- }
517
- post.likeState = reaction.likeState === 1 || reaction.likeState === 2 ? reaction.likeState : 0;
518
- }
519
- updateTopicPostHeader(post, topic);
520
- showNotification(state, post.likeState === 1 ? `已赞 ${post.floor ?? "?"} 楼` :
521
- post.likeState === 2 ? `已踩 ${post.floor ?? "?"} 楼` :
522
- `已取消 ${post.floor ?? "?"} 楼的赞踩`);
523
- state.status = getStatus(state);
524
- }
525
- catch (error) {
526
- state.error = error instanceof Error ? error.message : String(error);
527
- state.status = isLike ? "点赞失败" : "点踩失败";
528
- }
529
- finally {
530
- render();
531
- }
532
- }
533
- function updateTopicPostHeader(post, topic) {
534
- const floor = post.floor !== undefined ? `#${post.floor}` : "#?";
535
- const reaction = ` · ${post.likeCount} 赞 · ${post.dislikeCount} 踩`;
536
- const header = `${floor} ${post.author}${post.time ? ` · ${post.time}` : ""}${reaction}`;
537
- const headerLine = post.lines.find((entry) => entry.kind === "header");
538
- if (!headerLine) {
539
- return;
540
- }
541
- headerLine.text = header;
542
- if (headerLine.line >= 0 && headerLine.line < topic.lines.length) {
543
- topic.lines[headerLine.line] = header;
544
- }
545
- }
546
- function handleImageModal(context, key) {
547
- const { state, render } = context;
548
- if (!state.imageViewer) {
549
- state.modal = null;
550
- render();
551
- return;
552
- }
553
- if (key === " " || key === "\x1b" || key === "h" || key === "\r") {
554
- state.modal = null;
555
- state.status = getStatus(state);
556
- render();
557
- return;
558
- }
559
- if (key === "\x1b[C" || key === "l") {
560
- void stepTopicImageViewer(context, 1);
561
- return;
562
- }
563
- if (key === "\x1b[D" || key === "k") {
564
- void stepTopicImageViewer(context, -1);
565
- return;
566
- }
567
- }
568
- async function openTopicImageViewer(context) {
569
- const { state, render } = context;
570
- const topic = state.topic;
571
- if (!topic) {
572
- return;
573
- }
574
- if (!supportsImagePreview()) {
575
- state.status = "当前终端不支持图片大图预览";
576
- render();
577
- return;
578
- }
579
- const images = topic.posts
580
- .flatMap((post) => post.images)
581
- .filter((url) => !isEmotionAssetPath(url));
582
- if (images.length === 0) {
583
- state.status = "当前帖子没有可预览的大图";
584
- render();
585
- return;
586
- }
587
- const currentLine = currentTopicLine(topic, state.scroll);
588
- const currentPost = currentTopicPost(topic, state.scroll);
589
- const targetUrl = !isEmotionAssetPath(currentLine?.imageUrl ?? "")
590
- ? currentLine?.imageUrl
591
- : currentPost?.images.find((url) => !isEmotionAssetPath(url)) ?? images[0];
592
- const index = Math.max(0, images.findIndex((url) => url === targetUrl));
593
- state.imageViewer = {
594
- images,
595
- index,
596
- loading: true
597
- };
598
- state.modal = "image";
599
- render();
600
- await refreshTopicImageViewer(context, index);
601
- }
602
- async function stepTopicImageViewer(context, delta) {
603
- const viewer = context.state.imageViewer;
604
- if (!viewer || viewer.images.length === 0) {
605
- return;
606
- }
607
- const nextIndex = Math.min(viewer.images.length - 1, Math.max(0, viewer.index + delta));
608
- if (nextIndex === viewer.index && viewer.token) {
609
- return;
610
- }
611
- viewer.index = nextIndex;
612
- viewer.loading = true;
613
- viewer.error = undefined;
614
- viewer.token = undefined;
615
- viewer.renderSize = undefined;
616
- context.state.modal = "image";
617
- context.render();
618
- await refreshTopicImageViewer(context, nextIndex);
619
- }
620
- async function refreshTopicImageViewer(context, index) {
621
- const { state, render } = context;
622
- const viewer = state.imageViewer;
623
- if (!viewer) {
624
- return;
625
- }
626
- const terminalSize = process.stdout.isTTY
627
- ? { columns: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
628
- : { columns: 80, rows: 24 };
629
- const modalWidth = Math.max(24, Math.min(terminalSize.columns - 4, Math.floor(terminalSize.columns * 0.92)));
630
- const modalHeight = Math.max(10, Math.min(terminalSize.rows - 2, Math.floor(terminalSize.rows * 0.9)));
631
- const maxColumns = Math.max(1, modalWidth - 2);
632
- const maxRows = Math.max(1, modalHeight - 2);
633
- const url = viewer.images[index];
634
- try {
635
- const loadedImage = await loadModalImagePreview(url ?? "", maxColumns, maxRows);
636
- if (!state.imageViewer || state.imageViewer.index !== index) {
637
- return;
638
- }
639
- state.imageViewer.loading = false;
640
- state.imageViewer.token = loadedImage?.token;
641
- state.imageViewer.renderSize = loadedImage?.size;
642
- state.imageViewer.error = loadedImage ? undefined : "当前终端无法显示这张图片";
643
- }
644
- catch (error) {
645
- if (!state.imageViewer || state.imageViewer.index !== index) {
646
- return;
647
- }
648
- state.imageViewer.loading = false;
649
- state.imageViewer.token = undefined;
650
- state.imageViewer.renderSize = undefined;
651
- state.imageViewer.error = error instanceof Error ? error.message : "图片加载失败";
652
- }
653
- render();
654
- }
655
- function leaveTopicMode(state) {
656
- state.mode = "list";
657
- state.focus = "content";
658
- state.viewTitle = state.currentBoard?.title
659
- ?? state.currentChat?.title
660
- ?? state.currentSearch?.title
661
- ?? navItems[state.navIndex]?.label
662
- ?? state.viewTitle;
663
- state.status = getStatus(state);
664
- }
665
- function enterContentMode(state, resetIndex = false) {
666
- if (navItems[state.navIndex]?.id === "settings") {
667
- state.mode = "settings";
668
- }
669
- state.focus = "content";
670
- if (state.currentSearch && (resetIndex || state.items.length === 0)) {
671
- state.currentSearch.focus = "input";
672
- }
673
- if (resetIndex) {
674
- state.itemIndex = 0;
675
- }
676
- state.status = getStatus(state);
677
- }
678
- function leaveContentMode(state) {
679
- if (state.parentList) {
680
- restoreParentList(state);
681
- return;
682
- }
683
- state.mode = "list";
684
- state.focus = "nav";
685
- state.status = getStatus(state);
686
- }
687
- function openSelectedItem(context) {
688
- const { state, render, client, config, nextSignal } = context;
689
- const selected = state.items[state.itemIndex];
690
- if (selected?.topicId !== undefined) {
691
- void openTopic(client, state, selected.topicId, render, config, false, nextSignal());
692
- return true;
693
- }
694
- if (selected?.boardId !== undefined) {
695
- void openBoard(client, state, selected.boardId, selected.title, render, false, nextSignal());
696
- return true;
697
- }
698
- if (selected?.chatUserId !== undefined) {
699
- void openChat(client, state, selected.chatUserId, selected.title, render, false, nextSignal());
700
- return true;
701
- }
702
- return false;
703
- }
704
- async function handleTopicClick(context, event, columns, rows) {
705
- const { state, render } = context;
706
- if (state.mode !== "topic" || !state.topic || state.loading || state.error) {
707
- return;
708
- }
709
- const mainArea = getMainAreaRect(columns, rows, context.config, state.sidebarWidth);
710
- if (!withinRect(event.column, event.row, mainArea)) {
711
- return;
712
- }
713
- const bodyRow = event.row - (mainArea.y + 1);
714
- const bodyLineIndex = bodyRow - 3;
715
- if (bodyLineIndex < 0) {
716
- return;
717
- }
718
- const absoluteLine = state.scroll + bodyLineIndex;
719
- const lineEntry = state.topic.posts
720
- .flatMap((post) => post.lines)
721
- .find((entry) => entry.line === absoluteLine);
722
- const url = lineEntry?.linkUrl ?? lineEntry?.imageUrl;
723
- if (!url) {
724
- return;
725
- }
726
- state.status = `正在下载 ${shortUrl(url)}...`;
727
- render();
728
- try {
729
- const savedPath = await downloadUrlToDownloads(url);
730
- showNotification(state, `已下载到 ${savedPath}`);
731
- }
732
- catch (error) {
733
- state.status = error instanceof Error ? error.message : "下载失败";
734
- }
735
- finally {
736
- render();
737
- }
738
- }
739
- function handleSidebarClick(context, event, columns, rows) {
740
- const { state, render, load, abortCurrent } = context;
741
- if (state.mode === "topic") {
742
- return false;
743
- }
744
- const sidebarArea = getSidebarAreaRect(columns, rows, context.config, state.sidebarWidth);
745
- if (sidebarArea.width <= 0 || !withinRect(event.column, event.row, sidebarArea)) {
746
- return false;
747
- }
748
- const rowIndex = event.row - (sidebarArea.y + 1);
749
- if (rowIndex < 0 || rowIndex >= navItems.length) {
750
- return true;
751
- }
752
- const nextNavIndex = Math.max(0, Math.min(navItems.length - 1, rowIndex));
753
- if (state.navIndex === nextNavIndex && state.focus === "nav" && state.mode !== "settings") {
754
- return true;
755
- }
756
- abortCurrent();
757
- state.navIndex = nextNavIndex;
758
- state.focus = "nav";
759
- if (state.mode === "settings") {
760
- state.mode = "list";
761
- }
762
- state.status = getStatus(state);
763
- render();
764
- void load();
765
- return true;
766
- }
767
- function handleContentClick(context, event, columns, rows) {
768
- const { state, render } = context;
769
- if (state.mode === "topic" || state.loading || state.error) {
770
- return false;
771
- }
772
- const mainArea = getMainAreaRect(columns, rows, context.config, state.sidebarWidth);
773
- if (!withinRect(event.column, event.row, mainArea)) {
774
- return false;
775
- }
776
- if (navItems[state.navIndex]?.id === "settings" && state.mode !== "settings") {
777
- state.mode = "settings";
778
- }
779
- if (state.mode === "settings") {
780
- const rowIndex = event.row - (mainArea.y + 1) - 2;
781
- if (rowIndex < 0 || rowIndex >= settingsItems.length) {
782
- return true;
783
- }
784
- state.itemIndex = rowIndex;
785
- enterContentMode(state);
786
- render();
787
- return true;
788
- }
789
- if (state.currentSearch) {
790
- const rowIndex = event.row - (mainArea.y + 1);
791
- if (rowIndex === 1) {
792
- state.currentSearch.focus = "input";
793
- enterContentMode(state);
794
- render();
795
- return true;
796
- }
797
- if (rowIndex < 3) {
798
- return true;
799
- }
800
- const itemHeight = 2;
801
- const visibleCapacity = Math.max(1, Math.floor(Math.max(1, mainArea.height - 3) / itemHeight));
802
- const scroll = getContentListScroll(state, visibleCapacity);
803
- const itemOffset = Math.floor((rowIndex - 3) / itemHeight);
804
- const itemIndex = scroll + itemOffset;
805
- if (itemIndex < 0 || itemIndex >= state.items.length) {
806
- return true;
807
- }
808
- state.itemIndex = itemIndex;
809
- state.currentSearch.focus = "results";
810
- enterContentMode(state);
811
- render();
812
- return true;
813
- }
814
- const rowIndex = event.row - (mainArea.y + 1) - 2;
815
- if (rowIndex < 0) {
816
- return true;
817
- }
818
- const itemHeight = 2;
819
- if (rowIndex % itemHeight !== 0 && rowIndex % itemHeight !== 1) {
820
- return true;
821
- }
822
- const visibleCapacity = Math.max(1, Math.floor(Math.max(1, mainArea.height - 2) / itemHeight));
823
- const scroll = getContentListScroll(state, visibleCapacity);
824
- const itemOffset = Math.floor(rowIndex / itemHeight);
825
- const itemIndex = scroll + itemOffset;
826
- if (itemIndex < 0 || itemIndex >= state.items.length) {
827
- return true;
828
- }
829
- state.itemIndex = itemIndex;
830
- enterContentMode(state);
831
- render();
832
- return true;
833
- }
834
- function getMainAreaRect(columns, rows, config, sidebarWidthOverride) {
835
- const { mainArea } = getBodyColumnRects(columns, rows, config, sidebarWidthOverride);
836
- return mainArea;
837
- }
838
- function getSidebarAreaRect(columns, rows, config, sidebarWidthOverride) {
839
- const { sidebarArea } = getBodyColumnRects(columns, rows, config, sidebarWidthOverride);
840
- return sidebarArea;
841
- }
842
- function getBodyColumnRects(columns, rows, config, sidebarWidthOverride) {
843
- const width = Math.max(1, columns);
844
- const height = Math.max(1, rows);
845
- const outer = rect(width, Math.max(0, height - 1));
846
- const root = pad(outer, 1);
847
- const verticalLayout = config.hideTopChrome
848
- ? [fill()]
849
- : [length(1), length(1), length(1), length(1), fill()];
850
- const areas = split(root, "vertical", verticalLayout);
851
- const bodyArea = config.hideTopChrome ? areas[0] : areas[4];
852
- const sidebarWidth = getSidebarWidth(width, sidebarWidthOverride);
853
- const bodyColumns = split(bodyArea, "horizontal", [
854
- length(sidebarWidth),
855
- length(sidebarWidth > 0 ? 1 : 0),
856
- fill()
857
- ]);
858
- return {
859
- sidebarArea: bodyColumns[0],
860
- mainArea: bodyColumns[2]
861
- };
862
- }
863
- function getContentListScroll(state, visibleCapacity) {
864
- const maxScroll = Math.max(0, state.items.length - visibleCapacity);
865
- const current = Math.min(Math.max(0, state.scroll), maxScroll);
866
- if (state.itemIndex < current) {
867
- return state.itemIndex;
868
- }
869
- if (state.itemIndex >= current + visibleCapacity) {
870
- return Math.min(maxScroll, state.itemIndex - visibleCapacity + 1);
871
- }
872
- return current;
873
- }
874
- function withinRect(column, row, area) {
875
- const x = column - 1;
876
- const y = row - 1;
877
- return x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height;
878
- }
879
- function shortUrl(value) {
880
- try {
881
- const url = new URL(value);
882
- const fileName = url.pathname.split("/").filter(Boolean).at(-1) ?? url.host;
883
- return `${url.host}/${fileName}`;
884
- }
885
- catch {
886
- return value;
887
- }
888
- }
889
- function handleSettingsMode(context, key) {
890
- const { state, render, tokenStore, load } = context;
891
- if (key === "j" || key === "\x1b[B") {
892
- state.itemIndex = Math.min(settingsItems.length - 1, state.itemIndex + 1);
893
- render();
894
- return;
895
- }
896
- if (key === "k" || key === "\x1b[A") {
897
- state.itemIndex = Math.max(0, state.itemIndex - 1);
898
- render();
899
- return;
900
- }
901
- if (key === "h" || key === "\x1b[D") {
902
- leaveContentMode(state);
903
- render();
904
- return;
905
- }
906
- if (key !== "l" && key !== "\x1b[C" && key !== "\r") {
907
- return;
908
- }
909
- const selected = settingsItems[state.itemIndex];
910
- if (selected?.meta === "help") {
911
- state.modal = "help";
912
- render();
913
- return;
914
- }
915
- if (selected?.meta === "cache") {
916
- state.confirmDialog = {
917
- title: "缓存清理",
918
- detail: "清理过期文件缓存,并清空当前会话中的内存缓存?",
919
- confirmLabel: "确认清理",
920
- cancelLabel: "取消",
921
- selectedIndex: 1,
922
- action: "cache-cleanup"
923
- };
924
- state.modal = "confirm";
925
- render();
926
- return;
927
- }
928
- if (selected?.meta === "logout") {
929
- state.confirmDialog = {
930
- title: "退出登录",
931
- detail: state.account
932
- ? `删除本地账号 @${state.account} 的登录信息?`
933
- : "清除本地登录信息?",
934
- confirmLabel: "确认退出",
935
- cancelLabel: "取消",
936
- selectedIndex: 1,
937
- action: "logout"
938
- };
939
- state.modal = "confirm";
940
- render();
941
- return;
942
- }
943
- if (selected?.meta === "account") {
944
- void refreshAccounts(state, tokenStore).then(() => {
945
- if (state.accountModal.accounts.length === 0) {
946
- state.loginForm = createLoginForm();
947
- state.modal = "login";
948
- }
949
- else {
950
- state.modal = "account";
951
- }
952
- render();
953
- }).catch((error) => {
954
- state.error = error instanceof Error ? error.message : String(error);
955
- state.status = "读取账号列表失败";
956
- render();
957
- });
958
- return;
959
- }
960
- if (selected?.meta === "update") {
961
- state.status = "正在检查 GitHub Release...";
962
- render();
963
- void checkForUpdate().then((result) => {
964
- showNotification(state, result.message);
965
- render();
966
- }).catch((error) => {
967
- state.status = error instanceof Error ? error.message : "检查更新失败";
968
- render();
969
- });
970
- return;
971
- }
972
- void load(true);
973
- }
974
- function handleNavFocus(context, key) {
975
- const { state, render, load } = context;
976
- if (key === "j" || key === "\x1b[B") {
977
- state.navIndex = Math.min(navItems.length - 1, state.navIndex + 1);
978
- void load();
979
- return;
980
- }
981
- if (key === "k" || key === "\x1b[A") {
982
- state.navIndex = Math.max(0, state.navIndex - 1);
983
- void load();
984
- return;
985
- }
986
- if (key === "l" || key === "\x1b[C" || key === "\r") {
987
- if (!state.loading && (state.items.length > 0 || state.currentSearch)) {
988
- enterContentMode(state, key === "\r");
989
- render();
990
- }
991
- return;
992
- }
993
- if (key === "r") {
994
- void load(true);
995
- }
996
- }
997
- function handleContentFocus(context, key) {
998
- const { state, render, client, nextSignal, abortCurrent, load } = context;
999
- if (state.currentSearch) {
1000
- handleSearchContentFocus(context, key);
1001
- return;
1002
- }
1003
- if (key === "j" || key === "\x1b[B") {
1004
- state.itemIndex = Math.min(Math.max(0, state.items.length - 1), state.itemIndex + 1);
1005
- render();
1006
- return;
1007
- }
1008
- if (key === "k" || key === "\x1b[A") {
1009
- state.itemIndex = Math.max(0, state.itemIndex - 1);
1010
- render();
1011
- return;
1012
- }
1013
- if (key === "h" || key === "\x1b[D" || key === "\x1b") {
1014
- abortCurrent();
1015
- leaveContentMode(state);
1016
- render();
1017
- return;
1018
- }
1019
- if (key === "l" || key === "\x1b[C" || key === "\r") {
1020
- if (openSelectedItem(context)) {
1021
- return;
1022
- }
1023
- state.status = "当前条目不可进入";
1024
- render();
1025
- return;
1026
- }
1027
- if ((key === "n" || key === " ") && state.currentChat) {
1028
- void loadNextChatPage(client, state, render, nextSignal());
1029
- return;
1030
- }
1031
- if (key === "r") {
1032
- if (state.currentBoard) {
1033
- void openBoard(client, state, state.currentBoard.boardId, state.currentBoard.title, render, true, nextSignal(), false);
1034
- return;
1035
- }
1036
- if (state.currentChat) {
1037
- void openChat(client, state, state.currentChat.userId, state.currentChat.title, render, true, nextSignal(), false);
1038
- return;
1039
- }
1040
- void load(true);
1041
- return;
1042
- }
1043
- }
1044
- function handleSearchContentFocus(context, key) {
1045
- const { state, render, client, nextSignal, abortCurrent, load } = context;
1046
- const search = state.currentSearch;
1047
- if (!search) {
1048
- return;
1049
- }
1050
- if (key === "h" || key === "\x1b[D" || key === "\x1b") {
1051
- abortCurrent();
1052
- leaveContentMode(state);
1053
- render();
1054
- return;
1055
- }
1056
- if (search.focus === "input") {
1057
- if (key === "\x7f") {
1058
- search.draft = search.draft.slice(0, -1);
1059
- render();
1060
- return;
1061
- }
1062
- if (key === "\r") {
1063
- void executeSearch(client, state, render, false, nextSignal());
1064
- return;
1065
- }
1066
- if ((key === "j" || key === "\x1b[B" || key === "\t") && state.items.length > 0) {
1067
- search.focus = "results";
1068
- render();
1069
- return;
1070
- }
1071
- if (key === "r" && search.query) {
1072
- search.draft = search.query;
1073
- void executeSearch(client, state, render, true, nextSignal());
1074
- return;
1075
- }
1076
- if (isPrintableInput(key)) {
1077
- search.draft = `${search.draft}${key}`;
1078
- render();
1079
- }
1080
- return;
1081
- }
1082
- if (key === "i" || key === "/" || key === "\t") {
1083
- search.focus = "input";
1084
- render();
1085
- return;
1086
- }
1087
- if (key === "j" || key === "\x1b[B") {
1088
- const wasAtEnd = isAtSearchEnd(state);
1089
- state.itemIndex = Math.min(Math.max(0, state.items.length - 1), state.itemIndex + 1);
1090
- render();
1091
- if (wasAtEnd && search.hasMore && !state.loadingMore && !state.loading) {
1092
- void loadNextSearchPage(client, state, render, nextSignal());
1093
- }
1094
- return;
1095
- }
1096
- if (key === "k" || key === "\x1b[A") {
1097
- if (state.itemIndex === 0) {
1098
- search.focus = "input";
1099
- }
1100
- else {
1101
- state.itemIndex = Math.max(0, state.itemIndex - 1);
1102
- }
1103
- render();
1104
- return;
1105
- }
1106
- if (key === "l" || key === "\x1b[C" || key === "\r") {
1107
- if (openSelectedItem(context)) {
1108
- return;
1109
- }
1110
- state.status = "当前条目不可进入";
1111
- render();
1112
- return;
1113
- }
1114
- if (key === "n" || key === " ") {
1115
- void loadNextSearchPage(client, state, render, nextSignal());
1116
- return;
1117
- }
1118
- if (key === "r") {
1119
- if (search.query) {
1120
- search.draft = search.query;
1121
- void executeSearch(client, state, render, true, nextSignal());
1122
- return;
1123
- }
1124
- void load(true);
1125
- return;
1126
- }
1127
- if (isPrintableInput(key)) {
1128
- search.focus = "input";
1129
- search.draft = `${search.draft}${key}`;
1130
- render();
1131
- }
1132
- }
1
+ export { createKeyHandler } from "./app-runtime/keyboard.js";
2
+ export { createMouseHandler } from "./app-runtime/mouse.js";
1133
3
  //# sourceMappingURL=app-runtime.js.map