termicord 1.0.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/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/backend.ts +428 -0
- package/bun.lock +213 -0
- package/index.ts +605 -0
- package/middleware.ts +51 -0
- package/package.json +19 -0
- package/public/termicord.jpg +0 -0
- package/public/termicord.png +0 -0
- package/tsconfig.json +29 -0
package/index.ts
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
createCliRenderer,
|
|
4
|
+
InputRenderable,
|
|
5
|
+
InputRenderableEvents,
|
|
6
|
+
LayoutEvents,
|
|
7
|
+
TextRenderable,
|
|
8
|
+
type KeyEvent,
|
|
9
|
+
} from "@opentui/core";
|
|
10
|
+
|
|
11
|
+
import { startDownloadTask, type DownloadHandle } from "./middleware";
|
|
12
|
+
|
|
13
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
14
|
+
|
|
15
|
+
const c = {
|
|
16
|
+
lavender: "#c4b5fd",
|
|
17
|
+
purple: "#a78bfa",
|
|
18
|
+
violet: "#8b5cf6",
|
|
19
|
+
pink: "#f0abfc",
|
|
20
|
+
softPink: "#e879f9",
|
|
21
|
+
dim: "#6b7280",
|
|
22
|
+
dimBorder: "#374151",
|
|
23
|
+
focus: "#f9a8d4",
|
|
24
|
+
green: "#86efac",
|
|
25
|
+
dimGreen: "#4ade80",
|
|
26
|
+
transparent: "transparent",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function termW(): number {
|
|
30
|
+
return renderer.terminalWidth;
|
|
31
|
+
}
|
|
32
|
+
function termH(): number {
|
|
33
|
+
return renderer.terminalHeight;
|
|
34
|
+
}
|
|
35
|
+
function isWide(): boolean {
|
|
36
|
+
return termW() >= 120;
|
|
37
|
+
}
|
|
38
|
+
function isBannerFit(): boolean {
|
|
39
|
+
return termW() >= 84;
|
|
40
|
+
}
|
|
41
|
+
function logsHeight(): number {
|
|
42
|
+
return Math.max(6, termH() - 13);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const bannerLinesFull = [
|
|
46
|
+
"",
|
|
47
|
+
" ████████╗███████╗██████╗ ███╗ ███╗██╗ ██████╗ ██████╗ ██████╗ ██████╗ ",
|
|
48
|
+
" ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║██╔════╝██╔═══██╗██╔══██╗██╔══██╗",
|
|
49
|
+
" ██║ █████╗ ██████╔╝██╔████╔██║██║██║ ██║ ██║██████╔╝██║ ██║",
|
|
50
|
+
" ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║ ██║ ██║██╔══██╗██║ ██║",
|
|
51
|
+
" ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║╚██████╗╚██████╔╝██║ ██║██████╔╝",
|
|
52
|
+
" ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const bannerLinesCompact = [
|
|
56
|
+
"",
|
|
57
|
+
" ╔══════════════════════════╗",
|
|
58
|
+
" ║ TERMICORD ║",
|
|
59
|
+
" ╚══════════════════════════╝",
|
|
60
|
+
"",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function getBannerLines(): string[] {
|
|
64
|
+
return isBannerFit() ? bannerLinesFull : bannerLinesCompact;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const titleBanner = new TextRenderable(renderer, {
|
|
68
|
+
id: "title-banner",
|
|
69
|
+
content: getBannerLines()
|
|
70
|
+
.map(() => "")
|
|
71
|
+
.join("\n"),
|
|
72
|
+
fg: c.lavender,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const infoPanel = new BoxRenderable(renderer, {
|
|
76
|
+
id: "info-panel",
|
|
77
|
+
position: "absolute",
|
|
78
|
+
top: 1,
|
|
79
|
+
right: 2,
|
|
80
|
+
borderStyle: "double",
|
|
81
|
+
borderColor: c.transparent,
|
|
82
|
+
paddingLeft: 2,
|
|
83
|
+
paddingRight: 2,
|
|
84
|
+
paddingTop: 0,
|
|
85
|
+
paddingBottom: 0,
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const infoPanelLine1 = new TextRenderable(renderer, {
|
|
91
|
+
id: "info-line-0",
|
|
92
|
+
content: "v1.0.0 · MIT License",
|
|
93
|
+
fg: c.transparent,
|
|
94
|
+
});
|
|
95
|
+
const infoPanelLine2 = new TextRenderable(renderer, {
|
|
96
|
+
id: "info-line-1",
|
|
97
|
+
content: "──────────────────────",
|
|
98
|
+
fg: c.transparent,
|
|
99
|
+
});
|
|
100
|
+
const infoPanelLine3 = new TextRenderable(renderer, {
|
|
101
|
+
id: "info-line-2",
|
|
102
|
+
content: "developed & maintained by",
|
|
103
|
+
fg: c.transparent,
|
|
104
|
+
});
|
|
105
|
+
const infoPanelLine4 = new TextRenderable(renderer, {
|
|
106
|
+
id: "info-line-3",
|
|
107
|
+
content: " ♡ github / dilukshann7 ♡ ",
|
|
108
|
+
fg: c.transparent,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
infoPanel.add(infoPanelLine1);
|
|
112
|
+
infoPanel.add(infoPanelLine2);
|
|
113
|
+
infoPanel.add(infoPanelLine3);
|
|
114
|
+
infoPanel.add(infoPanelLine4);
|
|
115
|
+
|
|
116
|
+
const tabBar = new BoxRenderable(renderer, {
|
|
117
|
+
id: "tab-bar",
|
|
118
|
+
width: "100%" as `${number}%`,
|
|
119
|
+
height: 3,
|
|
120
|
+
borderStyle: "single",
|
|
121
|
+
borderColor: c.dimBorder,
|
|
122
|
+
paddingLeft: 1,
|
|
123
|
+
paddingRight: 1,
|
|
124
|
+
flexDirection: "row",
|
|
125
|
+
alignItems: "center",
|
|
126
|
+
justifyContent: "center",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const tabConfig = new TextRenderable(renderer, {
|
|
130
|
+
id: "tab-config",
|
|
131
|
+
content: " [ Config ] ",
|
|
132
|
+
fg: c.lavender,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const tabLogs = new TextRenderable(renderer, {
|
|
136
|
+
id: "tab-logs",
|
|
137
|
+
content: " Logs ",
|
|
138
|
+
fg: c.dim,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
tabBar.add(tabConfig);
|
|
142
|
+
tabBar.add(tabLogs);
|
|
143
|
+
|
|
144
|
+
const tokenPanel = new BoxRenderable(renderer, {
|
|
145
|
+
id: "token-panel",
|
|
146
|
+
width: "100%" as `${number}%`,
|
|
147
|
+
height: 3,
|
|
148
|
+
paddingLeft: 1,
|
|
149
|
+
borderColor: c.violet,
|
|
150
|
+
title: " Discord Token ",
|
|
151
|
+
});
|
|
152
|
+
const channelIDPanel = new BoxRenderable(renderer, {
|
|
153
|
+
id: "channel-id-panel",
|
|
154
|
+
width: "100%" as `${number}%`,
|
|
155
|
+
height: 3,
|
|
156
|
+
paddingLeft: 1,
|
|
157
|
+
borderColor: c.violet,
|
|
158
|
+
title: " Channel ID ",
|
|
159
|
+
});
|
|
160
|
+
const downloadLocationPanel = new BoxRenderable(renderer, {
|
|
161
|
+
id: "download-location-panel",
|
|
162
|
+
width: "100%" as `${number}%`,
|
|
163
|
+
height: 3,
|
|
164
|
+
paddingLeft: 1,
|
|
165
|
+
borderColor: c.violet,
|
|
166
|
+
title: " Download Location ",
|
|
167
|
+
});
|
|
168
|
+
const skipFilesInputPanel = new BoxRenderable(renderer, {
|
|
169
|
+
id: "skip-files-input-panel",
|
|
170
|
+
width: "100%" as `${number}%`,
|
|
171
|
+
height: 3,
|
|
172
|
+
paddingLeft: 1,
|
|
173
|
+
borderColor: c.violet,
|
|
174
|
+
title: " Extensions to Skip ",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const tokenInput = new InputRenderable(renderer, {
|
|
178
|
+
id: "token-input",
|
|
179
|
+
width: "100%" as `${number}%`,
|
|
180
|
+
placeholder: "Enter your Discord token...",
|
|
181
|
+
});
|
|
182
|
+
const channelIDInput = new InputRenderable(renderer, {
|
|
183
|
+
id: "channel-id-input",
|
|
184
|
+
width: "100%" as `${number}%`,
|
|
185
|
+
placeholder: "Enter channel ID...",
|
|
186
|
+
});
|
|
187
|
+
const downloadLocationInput = new InputRenderable(renderer, {
|
|
188
|
+
id: "download-location-input",
|
|
189
|
+
width: "100%" as `${number}%`,
|
|
190
|
+
placeholder: "Enter download location (e.g. ./downloads)...",
|
|
191
|
+
});
|
|
192
|
+
const skipFilesInput = new InputRenderable(renderer, {
|
|
193
|
+
id: "skip-files-input",
|
|
194
|
+
width: "100%" as `${number}%`,
|
|
195
|
+
placeholder: "Enter file extensions to skip (e.g. .jpg .png)...",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
tokenPanel.add(tokenInput);
|
|
199
|
+
channelIDPanel.add(channelIDInput);
|
|
200
|
+
downloadLocationPanel.add(downloadLocationInput);
|
|
201
|
+
skipFilesInputPanel.add(skipFilesInput);
|
|
202
|
+
|
|
203
|
+
let checked = false;
|
|
204
|
+
|
|
205
|
+
const checkbox = new TextRenderable(renderer, {
|
|
206
|
+
id: "checkbox",
|
|
207
|
+
content: " [ ] Create a new folder for every message",
|
|
208
|
+
fg: c.dim,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const downloadButton = new BoxRenderable(renderer, {
|
|
212
|
+
id: "download-button",
|
|
213
|
+
position: "absolute",
|
|
214
|
+
bottom: 3, // sits directly on top of the hint bar
|
|
215
|
+
width: "100%" as `${number}%`,
|
|
216
|
+
height: 3,
|
|
217
|
+
borderStyle: "double",
|
|
218
|
+
borderColor: c.softPink,
|
|
219
|
+
paddingLeft: 1,
|
|
220
|
+
paddingRight: 1,
|
|
221
|
+
flexDirection: "row",
|
|
222
|
+
justifyContent: "center",
|
|
223
|
+
alignItems: "center",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const downloadButtonText = new TextRenderable(renderer, {
|
|
227
|
+
id: "download-button-text",
|
|
228
|
+
content: " Start Download ",
|
|
229
|
+
fg: c.pink,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
downloadButton.add(downloadButtonText);
|
|
233
|
+
|
|
234
|
+
const logsBox = new BoxRenderable(renderer, {
|
|
235
|
+
id: "logs-box",
|
|
236
|
+
width: "100%" as `${number}%`,
|
|
237
|
+
height: logsHeight(),
|
|
238
|
+
borderStyle: "single",
|
|
239
|
+
borderColor: c.dimBorder,
|
|
240
|
+
title: " Logs ",
|
|
241
|
+
paddingLeft: 1,
|
|
242
|
+
paddingRight: 1,
|
|
243
|
+
flexDirection: "column",
|
|
244
|
+
overflow: "scroll",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const logsText = new TextRenderable(renderer, {
|
|
248
|
+
id: "logs-text",
|
|
249
|
+
content: " Waiting for download to start...",
|
|
250
|
+
fg: c.dim,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
logsBox.add(logsText);
|
|
254
|
+
|
|
255
|
+
const hintBar = new BoxRenderable(renderer, {
|
|
256
|
+
id: "hint-bar",
|
|
257
|
+
position: "absolute",
|
|
258
|
+
bottom: 0,
|
|
259
|
+
width: "100%" as `${number}%`,
|
|
260
|
+
height: 3,
|
|
261
|
+
borderStyle: "single",
|
|
262
|
+
borderColor: c.dimBorder,
|
|
263
|
+
paddingLeft: 2,
|
|
264
|
+
paddingRight: 2,
|
|
265
|
+
flexDirection: "row",
|
|
266
|
+
justifyContent: "center",
|
|
267
|
+
alignItems: "center",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const hintFull =
|
|
271
|
+
"Ctrl + Q/E · switch tabs | Tab · next field | Shift + Tab · prev | Space · toggle | Enter · download | Esc · abort | Ctrl+C · exit";
|
|
272
|
+
const hintCompact =
|
|
273
|
+
"Ctrl + Q/E tabs | Tab/S-Tab nav | Space toggle | Enter go | Esc abort | Ctrl+C quit";
|
|
274
|
+
|
|
275
|
+
const hint = new TextRenderable(renderer, {
|
|
276
|
+
id: "hint",
|
|
277
|
+
content: termW() >= 120 ? hintFull : hintCompact,
|
|
278
|
+
fg: c.dim,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
hintBar.add(hint);
|
|
282
|
+
|
|
283
|
+
type Tab = "config" | "logs";
|
|
284
|
+
let activeTab: Tab = "config";
|
|
285
|
+
|
|
286
|
+
const configChildren = [
|
|
287
|
+
tokenPanel,
|
|
288
|
+
channelIDPanel,
|
|
289
|
+
downloadLocationPanel,
|
|
290
|
+
skipFilesInputPanel,
|
|
291
|
+
checkbox,
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
function showTab(tab: Tab) {
|
|
295
|
+
if (activeTab === tab) return;
|
|
296
|
+
activeTab = tab;
|
|
297
|
+
|
|
298
|
+
tabConfig.fg = tab === "config" ? c.lavender : c.dim;
|
|
299
|
+
tabLogs.fg = tab === "logs" ? c.lavender : c.dim;
|
|
300
|
+
tabConfig.content = tab === "config" ? " [ Config ] " : " Config ";
|
|
301
|
+
tabLogs.content = tab === "logs" ? " [ Logs ] " : " Logs ";
|
|
302
|
+
|
|
303
|
+
if (tab === "config") {
|
|
304
|
+
logsBox.visible = false;
|
|
305
|
+
configChildren.forEach((child) => {
|
|
306
|
+
child.visible = true;
|
|
307
|
+
});
|
|
308
|
+
downloadButton.visible = true;
|
|
309
|
+
focusAt(0);
|
|
310
|
+
} else {
|
|
311
|
+
configChildren.forEach((child) => {
|
|
312
|
+
child.visible = false;
|
|
313
|
+
});
|
|
314
|
+
downloadButton.visible = false;
|
|
315
|
+
inputs.forEach((inp) => inp.blur());
|
|
316
|
+
logsBox.visible = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 0-3 = inputs, 4 = checkbox
|
|
321
|
+
const TOTAL_FIELDS = 5;
|
|
322
|
+
const inputPanels = [
|
|
323
|
+
tokenPanel,
|
|
324
|
+
channelIDPanel,
|
|
325
|
+
downloadLocationPanel,
|
|
326
|
+
skipFilesInputPanel,
|
|
327
|
+
];
|
|
328
|
+
const inputs = [
|
|
329
|
+
tokenInput,
|
|
330
|
+
channelIDInput,
|
|
331
|
+
downloadLocationInput,
|
|
332
|
+
skipFilesInput,
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
let focusedIndex = 0;
|
|
336
|
+
let animationDone = false;
|
|
337
|
+
|
|
338
|
+
function updateFocusStyles() {
|
|
339
|
+
inputPanels.forEach((panel, i) => {
|
|
340
|
+
panel.borderColor = focusedIndex === i ? c.focus : c.violet;
|
|
341
|
+
});
|
|
342
|
+
checkbox.fg = focusedIndex === 4 ? c.focus : c.dim;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function focusAt(index: number) {
|
|
346
|
+
focusedIndex = ((index % TOTAL_FIELDS) + TOTAL_FIELDS) % TOTAL_FIELDS;
|
|
347
|
+
if (focusedIndex < 4) {
|
|
348
|
+
inputs.forEach((inp) => inp.blur());
|
|
349
|
+
(inputs[focusedIndex] as InputRenderable).focus();
|
|
350
|
+
} else {
|
|
351
|
+
inputs.forEach((inp) => inp.blur());
|
|
352
|
+
}
|
|
353
|
+
updateFocusStyles();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const logLines: string[] = [];
|
|
357
|
+
|
|
358
|
+
function addLog(line: string) {
|
|
359
|
+
const now = new Date();
|
|
360
|
+
const ts = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`;
|
|
361
|
+
logLines.push(` [${ts}] ${line}`);
|
|
362
|
+
logsText.content = logLines.join("\n");
|
|
363
|
+
logsText.fg = c.green;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let activeDownload: DownloadHandle | null = null;
|
|
367
|
+
let isDownloading = false;
|
|
368
|
+
|
|
369
|
+
function setDownloading(active: boolean) {
|
|
370
|
+
isDownloading = active;
|
|
371
|
+
if (active) {
|
|
372
|
+
downloadButtonText.content = " ♡ Downloading… ♡ ";
|
|
373
|
+
downloadButtonText.fg = c.dimGreen;
|
|
374
|
+
downloadButton.borderColor = c.dimGreen;
|
|
375
|
+
} else {
|
|
376
|
+
downloadButtonText.content = " ♡ Start Download ♡ ";
|
|
377
|
+
downloadButtonText.fg = c.pink;
|
|
378
|
+
downloadButton.borderColor = c.softPink;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function startDownload() {
|
|
383
|
+
if (isDownloading) {
|
|
384
|
+
addLog("⚠ Download already in progress.");
|
|
385
|
+
showTab("logs");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const token = (tokenInput as any).value ?? "";
|
|
390
|
+
const channel = (channelIDInput as any).value ?? "";
|
|
391
|
+
const location = (downloadLocationInput as any).value || "./downloads";
|
|
392
|
+
const skip = (skipFilesInput as any).value ?? "";
|
|
393
|
+
|
|
394
|
+
if (!token || !channel) {
|
|
395
|
+
addLog("✗ Missing required fields (token and channel ID).");
|
|
396
|
+
showTab("logs");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
addLog(`♡ Starting download...`);
|
|
401
|
+
addLog(
|
|
402
|
+
` Token : ${token.slice(0, 8)}${"*".repeat(Math.max(0, token.length - 8))}`,
|
|
403
|
+
);
|
|
404
|
+
addLog(` Channel : ${channel}`);
|
|
405
|
+
addLog(` Location : ${location}`);
|
|
406
|
+
addLog(` Skip ext : ${skip || "(none)"}`);
|
|
407
|
+
addLog(` Folders : ${checked ? "yes (one per message)" : "no"}`);
|
|
408
|
+
addLog(`──────────────────────────────────────────────`);
|
|
409
|
+
|
|
410
|
+
showTab("logs");
|
|
411
|
+
setDownloading(true);
|
|
412
|
+
|
|
413
|
+
activeDownload = startDownloadTask(
|
|
414
|
+
{
|
|
415
|
+
token,
|
|
416
|
+
channelId: channel,
|
|
417
|
+
outputDir: location,
|
|
418
|
+
skipExtensions: skip,
|
|
419
|
+
foldersPerMessage: checked,
|
|
420
|
+
},
|
|
421
|
+
(line) => addLog(line),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
activeDownload.done.then(() => {
|
|
425
|
+
setDownloading(false);
|
|
426
|
+
activeDownload = null;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function abortDownload() {
|
|
431
|
+
if (activeDownload && isDownloading) {
|
|
432
|
+
activeDownload.abort();
|
|
433
|
+
addLog("⊘ Download aborted by user.");
|
|
434
|
+
setDownloading(false);
|
|
435
|
+
activeDownload = null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function handleResize() {
|
|
440
|
+
if (animationDone) {
|
|
441
|
+
titleBanner.content = getBannerLines().join("\n");
|
|
442
|
+
infoPanel.visible = isWide();
|
|
443
|
+
}
|
|
444
|
+
logsBox.height = logsHeight();
|
|
445
|
+
hint.content = termW() >= 120 ? hintFull : hintCompact;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
renderer.root.on(LayoutEvents.RESIZED, handleResize);
|
|
449
|
+
|
|
450
|
+
renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
451
|
+
if (!animationDone) return;
|
|
452
|
+
|
|
453
|
+
const anyInputFocused =
|
|
454
|
+
focusedIndex >= 0 && focusedIndex < 4 && activeTab === "config";
|
|
455
|
+
|
|
456
|
+
if (!anyInputFocused) {
|
|
457
|
+
if (key.name === "q" || key.name === "Q") {
|
|
458
|
+
showTab("config");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (key.name === "e" || key.name === "E") {
|
|
462
|
+
showTab("logs");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (key.ctrl && (key.name === "q" || key.name === "Q")) {
|
|
468
|
+
showTab("config");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key.ctrl && (key.name === "e" || key.name === "E")) {
|
|
472
|
+
showTab("logs");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (key.name === "escape" && isDownloading) {
|
|
477
|
+
abortDownload();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (activeTab === "config") {
|
|
482
|
+
if (key.name === "tab") {
|
|
483
|
+
key.stopPropagation();
|
|
484
|
+
focusAt(key.shift ? focusedIndex - 1 : focusedIndex + 1);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (key.name === "space" && focusedIndex === 4) {
|
|
488
|
+
checked = !checked;
|
|
489
|
+
checkbox.content = ` [${checked ? "♡" : " "}] Create a new folder for every message`;
|
|
490
|
+
}
|
|
491
|
+
if (key.name === "return" && focusedIndex === 4) {
|
|
492
|
+
startDownload();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const onFieldEnter = () => startDownload();
|
|
498
|
+
tokenInput.on(InputRenderableEvents.ENTER, onFieldEnter);
|
|
499
|
+
channelIDInput.on(InputRenderableEvents.ENTER, onFieldEnter);
|
|
500
|
+
downloadLocationInput.on(InputRenderableEvents.ENTER, onFieldEnter);
|
|
501
|
+
skipFilesInput.on(InputRenderableEvents.ENTER, onFieldEnter);
|
|
502
|
+
|
|
503
|
+
downloadButton.onMouseDown = () => {
|
|
504
|
+
if (isDownloading) {
|
|
505
|
+
abortDownload();
|
|
506
|
+
} else {
|
|
507
|
+
startDownload();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
renderer.root.add(titleBanner);
|
|
512
|
+
|
|
513
|
+
function animateBanner(onComplete: () => void) {
|
|
514
|
+
const lines = getBannerLines();
|
|
515
|
+
const revealedLines: string[] = lines.map(() => "");
|
|
516
|
+
lines.forEach((line, i) => {
|
|
517
|
+
setTimeout(() => {
|
|
518
|
+
revealedLines[i] = line;
|
|
519
|
+
titleBanner.content = revealedLines.join("\n");
|
|
520
|
+
if (i === lines.length - 1) onComplete();
|
|
521
|
+
}, i * 55);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
animateBanner(() => {
|
|
526
|
+
if (isWide()) {
|
|
527
|
+
infoPanel.borderColor = c.purple;
|
|
528
|
+
infoPanelLine1.fg = c.transparent;
|
|
529
|
+
infoPanelLine2.fg = c.transparent;
|
|
530
|
+
infoPanelLine3.fg = c.transparent;
|
|
531
|
+
infoPanelLine4.fg = c.transparent;
|
|
532
|
+
renderer.root.add(infoPanel);
|
|
533
|
+
const id = 80;
|
|
534
|
+
setTimeout(() => {
|
|
535
|
+
infoPanelLine1.fg = c.lavender;
|
|
536
|
+
}, id * 1);
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
infoPanelLine2.fg = c.violet;
|
|
539
|
+
}, id * 2);
|
|
540
|
+
setTimeout(() => {
|
|
541
|
+
infoPanelLine3.fg = c.purple;
|
|
542
|
+
}, id * 3);
|
|
543
|
+
setTimeout(() => {
|
|
544
|
+
infoPanelLine4.fg = c.pink;
|
|
545
|
+
}, id * 4);
|
|
546
|
+
} else {
|
|
547
|
+
infoPanel.visible = false;
|
|
548
|
+
renderer.root.add(infoPanel);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
tabBar.visible = false;
|
|
552
|
+
configChildren.forEach((child) => {
|
|
553
|
+
child.visible = false;
|
|
554
|
+
});
|
|
555
|
+
logsBox.visible = false;
|
|
556
|
+
downloadButton.visible = false;
|
|
557
|
+
hintBar.visible = false;
|
|
558
|
+
|
|
559
|
+
renderer.root.add(tabBar);
|
|
560
|
+
configChildren.forEach((child) => renderer.root.add(child));
|
|
561
|
+
renderer.root.add(logsBox);
|
|
562
|
+
renderer.root.add(downloadButton);
|
|
563
|
+
renderer.root.add(hintBar);
|
|
564
|
+
|
|
565
|
+
const pd = 120;
|
|
566
|
+
setTimeout(() => {
|
|
567
|
+
tabBar.visible = true;
|
|
568
|
+
}, pd * 1);
|
|
569
|
+
setTimeout(() => {
|
|
570
|
+
tokenPanel.visible = true;
|
|
571
|
+
}, pd * 2);
|
|
572
|
+
setTimeout(() => {
|
|
573
|
+
channelIDPanel.visible = true;
|
|
574
|
+
}, pd * 3);
|
|
575
|
+
setTimeout(() => {
|
|
576
|
+
downloadLocationPanel.visible = true;
|
|
577
|
+
}, pd * 4);
|
|
578
|
+
setTimeout(() => {
|
|
579
|
+
skipFilesInputPanel.visible = true;
|
|
580
|
+
}, pd * 5);
|
|
581
|
+
setTimeout(() => {
|
|
582
|
+
checkbox.visible = true;
|
|
583
|
+
}, pd * 6);
|
|
584
|
+
setTimeout(
|
|
585
|
+
() => {
|
|
586
|
+
downloadButton.visible = true;
|
|
587
|
+
},
|
|
588
|
+
pd * 6 + 120,
|
|
589
|
+
);
|
|
590
|
+
setTimeout(
|
|
591
|
+
() => {
|
|
592
|
+
hintBar.visible = true;
|
|
593
|
+
},
|
|
594
|
+
pd * 6 + 220,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
setTimeout(
|
|
598
|
+
() => {
|
|
599
|
+
animationDone = true;
|
|
600
|
+
focusAt(0);
|
|
601
|
+
handleResize();
|
|
602
|
+
},
|
|
603
|
+
pd * 6 + 320,
|
|
604
|
+
);
|
|
605
|
+
});
|
package/middleware.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runDownload,
|
|
3
|
+
parseSkipExtensions,
|
|
4
|
+
type DownloadConfig,
|
|
5
|
+
type ProgressCallback,
|
|
6
|
+
} from "./backend";
|
|
7
|
+
|
|
8
|
+
export interface DownloadConfigRaw {
|
|
9
|
+
token: string;
|
|
10
|
+
channelId: string;
|
|
11
|
+
outputDir: string;
|
|
12
|
+
skipExtensions: string;
|
|
13
|
+
foldersPerMessage: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DownloadHandle {
|
|
17
|
+
done: Promise<void>;
|
|
18
|
+
abort: () => void;
|
|
19
|
+
readonly aborted: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function startDownloadTask(
|
|
23
|
+
config: DownloadConfigRaw,
|
|
24
|
+
onProgress: (line: string) => void,
|
|
25
|
+
): DownloadHandle {
|
|
26
|
+
const ac = new AbortController();
|
|
27
|
+
|
|
28
|
+
const resolved: DownloadConfig = {
|
|
29
|
+
token: config.token,
|
|
30
|
+
channelId: config.channelId,
|
|
31
|
+
outputDir: config.outputDir || "./downloads",
|
|
32
|
+
skipExtensions: parseSkipExtensions(config.skipExtensions),
|
|
33
|
+
foldersPerMessage: config.foldersPerMessage,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const progressCb: ProgressCallback = (evt) => {
|
|
37
|
+
onProgress(evt.message);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const done = runDownload(resolved, progressCb, ac.signal).catch((err) => {
|
|
41
|
+
onProgress(`✗ Unexpected error: ${(err as Error).message}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
done,
|
|
46
|
+
abort: () => ac.abort(),
|
|
47
|
+
get aborted() {
|
|
48
|
+
return ac.signal.aborted;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "termicord",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"termicord": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"private": false,
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"typescript": "^5"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@opentui/core": "^0.1.87"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
Binary file
|
|
Binary file
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|