eva-md 0.4.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eva.js +333 -189
- package/package.json +1 -1
package/eva.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* EVA.md v0.
|
|
3
|
+
* EVA.md v0.6.1 — EVA Media Downloader by .maxon
|
|
4
4
|
* Pure Node.js Interactive TUI (Zero external npm dependencies!)
|
|
5
5
|
* Works just like Gemini CLI (`node eva.js` or `./eva.js`), with full mouse & keyboard support.
|
|
6
6
|
*
|
|
@@ -46,11 +46,12 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
46
46
|
// Constants & Configuration
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
|
|
49
|
-
const APP_VERSION = 'v0.
|
|
49
|
+
const APP_VERSION = 'v0.6.1 (Node.js)';
|
|
50
50
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'eva_md', 'config.json');
|
|
51
51
|
const MAX_LOG_LINES = 200;
|
|
52
52
|
const MAX_LOG_LINE_LENGTH = 500;
|
|
53
53
|
const MAX_DOWNLOAD_RETRIES = 3;
|
|
54
|
+
const LIBRARY_DOUBLE_CLICK_MS = 400;
|
|
54
55
|
const ANSI_ESCAPE_RE = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)|[@-_])/g;
|
|
55
56
|
|
|
56
57
|
// Strictly 1-cell narrow glyphs (CP437/WGL4 safe) so Windows Terminal / PowerShell NEVER shifts borders!
|
|
@@ -78,7 +79,7 @@ const BROWSERS = ['none', 'chrome', 'firefox', 'edge', 'brave', 'opera', 'vivald
|
|
|
78
79
|
const NAMING_PRESETS = [
|
|
79
80
|
{ label: 'Плейлист: 01. Название файла.mp4(формат)', tpl: '%(playlist_index)s. %(title)s.%(ext)s' },
|
|
80
81
|
{ label: 'Стандарт: Название файла.mp4(формат)', tpl: '%(title)s.%(ext)s' },
|
|
81
|
-
{ label: '
|
|
82
|
+
{ label: 'Автор: Автор - Название.mp4(формат)', tpl: '%(uploader)s - %(title)s.%(ext)s' },
|
|
82
83
|
];
|
|
83
84
|
|
|
84
85
|
const ASCII_LOGO = [
|
|
@@ -104,7 +105,6 @@ const ANSI = {
|
|
|
104
105
|
fgBlack: `${CSI}30m`,
|
|
105
106
|
fgGray: `${CSI}38;2;105;105;105m`,
|
|
106
107
|
fgOrange: `${CSI}38;2;222;91;9m`, // Текст цвета RGB(222, 91, 9)
|
|
107
|
-
bgOrange: `${CSI}48;2;222;91;9m`,
|
|
108
108
|
bgGray: `${CSI}48;2;105;105;105m`,
|
|
109
109
|
bgWhite: `${CSI}47m`,
|
|
110
110
|
bgCyan: `${CSI}46m`,
|
|
@@ -143,6 +143,10 @@ function shellQuote(arg) {
|
|
|
143
143
|
return arg;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
function resolveOutputDir(outputDir) {
|
|
147
|
+
return outputDir.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
148
|
+
}
|
|
149
|
+
|
|
146
150
|
function formatBytesToMB(bytes) {
|
|
147
151
|
if (!bytes || bytes <= 0) return '—';
|
|
148
152
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
@@ -341,7 +345,7 @@ function buildYtdlpArgs(opts, targetUrl = null) {
|
|
|
341
345
|
if (opts.writeThumbnail) args.push('--write-thumbnail');
|
|
342
346
|
|
|
343
347
|
// File Naming & Archive
|
|
344
|
-
const outDir = opts.outputDir
|
|
348
|
+
const outDir = resolveOutputDir(opts.outputDir);
|
|
345
349
|
const tpl = opts.usePlaylistTemplate ? opts.outputTemplate : '%(title)s.%(ext)s';
|
|
346
350
|
args.push('-o', path.join(outDir, tpl));
|
|
347
351
|
|
|
@@ -480,7 +484,7 @@ class DownloadManager {
|
|
|
480
484
|
_createQueueItem(opts, url, title = null, entry = null, entryIndex = 0) {
|
|
481
485
|
const src = detectSource(url, opts.sourceId);
|
|
482
486
|
const fmt = opts.bestVideoMerge && !opts.extractAudio ? 'mp4/mkv' : opts.audioFormat;
|
|
483
|
-
const outDir = opts.outputDir
|
|
487
|
+
const outDir = resolveOutputDir(opts.outputDir);
|
|
484
488
|
const itemOpts = { ...opts, usePlaylistItems: false, playlistReverse: false };
|
|
485
489
|
if (entry && itemOpts.usePlaylistTemplate) {
|
|
486
490
|
const playlistIndex = String(entry.playlist_index || entryIndex + 1).padStart(2, '0');
|
|
@@ -987,6 +991,7 @@ class ScreenBuffer {
|
|
|
987
991
|
constructor(cols, rows) {
|
|
988
992
|
this.cols = cols;
|
|
989
993
|
this.rows = rows;
|
|
994
|
+
this.clipRight = null;
|
|
990
995
|
this.chars = Array.from({ length: rows }, () => Array(cols).fill(' '));
|
|
991
996
|
this.styles = Array.from({ length: rows }, () => Array(cols).fill(ANSI.fgWhite));
|
|
992
997
|
}
|
|
@@ -996,7 +1001,7 @@ class ScreenBuffer {
|
|
|
996
1001
|
const str = String(text);
|
|
997
1002
|
for (let i = 0; i < str.length; i++) {
|
|
998
1003
|
const cx = x + i;
|
|
999
|
-
if (cx >= 0 && cx < this.cols - 1) {
|
|
1004
|
+
if (cx >= 0 && cx < this.cols - 1 && (this.clipRight === null || cx <= this.clipRight)) {
|
|
1000
1005
|
this.chars[y][cx] = str[i];
|
|
1001
1006
|
this.styles[y][cx] = style;
|
|
1002
1007
|
}
|
|
@@ -1053,10 +1058,15 @@ class EvaNodeTUI {
|
|
|
1053
1058
|
this.hitboxes = [];
|
|
1054
1059
|
this.selectedQueueIdx = 0;
|
|
1055
1060
|
this.selectedQueueItem = null;
|
|
1061
|
+
this.queueScrollOffset = 0;
|
|
1062
|
+
this.queueViewport = null;
|
|
1063
|
+
this.libraryScrollOffset = 0;
|
|
1064
|
+
this.libraryViewport = null;
|
|
1065
|
+
this.selectedLibraryFile = null;
|
|
1066
|
+
this.lastLibraryClick = null;
|
|
1056
1067
|
this.statusBanner = '';
|
|
1057
1068
|
|
|
1058
|
-
|
|
1059
|
-
this.modal = null; // { label, value, cursor, onConfirm }
|
|
1069
|
+
this.modal = null;
|
|
1060
1070
|
}
|
|
1061
1071
|
|
|
1062
1072
|
start() {
|
|
@@ -1081,101 +1091,84 @@ class EvaNodeTUI {
|
|
|
1081
1091
|
process.exit(0);
|
|
1082
1092
|
}
|
|
1083
1093
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
style = id === 'url_input'
|
|
1113
|
-
? ANSI.fgWhite + ANSI.bold
|
|
1114
|
-
: id === 'btn_add_queue'
|
|
1115
|
-
? ANSI.fgGreen + ANSI.bold
|
|
1116
|
-
: ANSI.fgCyan + ANSI.bold;
|
|
1117
|
-
} else if (checked || radio) {
|
|
1118
|
-
style = ANSI.fgOrange + ANSI.bold;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// Вывод префикса с раздельными цветами
|
|
1122
|
-
if (checked !== null) {
|
|
1123
|
-
scr.putStr(y, x, '[', ANSI.fgGray);
|
|
1094
|
+
registerWidget(scr, id, y, x, label, action, { checked = null, radio = null, isCta = false, maxWidth = 0 } = {}) {
|
|
1095
|
+
this.focusOrder.push(id);
|
|
1096
|
+
const isFocused = this.focusId === id;
|
|
1097
|
+
let prefix = '';
|
|
1098
|
+
|
|
1099
|
+
if (checked !== null) {
|
|
1100
|
+
prefix = checked ? '[x] ' : '[ ] ';
|
|
1101
|
+
} else if (radio !== null) {
|
|
1102
|
+
prefix = radio ? '(•) ' : '( ) ';
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
let style = ANSI.fgGray;
|
|
1106
|
+
if (isFocused) {
|
|
1107
|
+
style = isCta
|
|
1108
|
+
? ANSI.bgGray + ANSI.fgWhite + ANSI.bold
|
|
1109
|
+
: ANSI.bgWhite + ANSI.fgBlack + ANSI.bold;
|
|
1110
|
+
} else if (isCta) {
|
|
1111
|
+
style = ['url_input', 'edit_proxy', 'cycle_browser'].includes(id)
|
|
1112
|
+
? ANSI.fgWhite + ANSI.bold
|
|
1113
|
+
: id === 'btn_add_queue'
|
|
1114
|
+
? ANSI.fgGreen + ANSI.bold
|
|
1115
|
+
: ANSI.fgCyan + ANSI.bold;
|
|
1116
|
+
} else if (checked || radio) {
|
|
1117
|
+
style = ANSI.fgOrange + ANSI.bold;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (checked !== null) {
|
|
1121
|
+
scr.putStr(y, x, '[', ANSI.fgGray);
|
|
1124
1122
|
scr.putStr(y, x + 1, checked ? 'x' : ' ', checked ? ANSI.fgOrange + ANSI.bold : ANSI.dim);
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// Расчёт доступной ширины текста
|
|
1133
|
-
const availableWidth = maxWidth > 0
|
|
1134
|
-
? Math.max(0, maxWidth - prefix.length)
|
|
1135
|
-
: Infinity;
|
|
1136
|
-
|
|
1137
|
-
let visibleLabel = label;
|
|
1123
|
+
scr.putStr(y, x + 2, '] ', ANSI.fgGray);
|
|
1124
|
+
} else if (radio !== null) {
|
|
1125
|
+
scr.putStr(y, x, '(', ANSI.fgGray);
|
|
1126
|
+
scr.putStr(y, x + 1, radio ? '•' : ' ', radio ? ANSI.fgOrange + ANSI.bold : ANSI.dim);
|
|
1127
|
+
scr.putStr(y, x + 2, ') ', ANSI.fgGray);
|
|
1128
|
+
}
|
|
1138
1129
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1130
|
+
const availableWidth = maxWidth > 0 ? Math.max(0, maxWidth - prefix.length) : Infinity;
|
|
1131
|
+
let visibleLabel = label;
|
|
1132
|
+
if (visibleLabel.length > availableWidth) {
|
|
1133
|
+
visibleLabel = availableWidth > 0 ? visibleLabel.slice(0, availableWidth - 1) + '…' : '';
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const textX = x + prefix.length;
|
|
1137
|
+
const editMarker = '[✎ ]';
|
|
1138
|
+
const editMarkerIndex = visibleLabel.lastIndexOf(editMarker);
|
|
1139
|
+
if (!isFocused && editMarkerIndex >= 0) {
|
|
1140
|
+
const folderLabel = 'Папка:';
|
|
1141
|
+
const folderLabelIndex = visibleLabel.indexOf(folderLabel);
|
|
1142
|
+
if (folderLabelIndex >= 0 && folderLabelIndex < editMarkerIndex) {
|
|
1143
|
+
scr.putStr(y, textX, visibleLabel.slice(0, folderLabelIndex), style);
|
|
1144
|
+
scr.putStr(y, textX + folderLabelIndex, folderLabel, ANSI.fgWhite);
|
|
1145
|
+
const afterFolderIndex = folderLabelIndex + folderLabel.length;
|
|
1146
|
+
scr.putStr(y, textX + afterFolderIndex, visibleLabel.slice(afterFolderIndex, editMarkerIndex), style);
|
|
1147
|
+
} else {
|
|
1148
|
+
scr.putStr(y, textX, visibleLabel.slice(0, editMarkerIndex), style);
|
|
1149
|
+
}
|
|
1150
|
+
scr.putStr(y, textX + editMarkerIndex, editMarker, ANSI.fgWhite);
|
|
1151
|
+
scr.putStr(
|
|
1152
|
+
y,
|
|
1153
|
+
textX + editMarkerIndex + editMarker.length,
|
|
1154
|
+
visibleLabel.slice(editMarkerIndex + editMarker.length),
|
|
1155
|
+
style
|
|
1156
|
+
);
|
|
1157
1157
|
} else {
|
|
1158
|
-
scr.putStr(y, textX, visibleLabel
|
|
1158
|
+
scr.putStr(y, textX, visibleLabel, style);
|
|
1159
1159
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1160
|
+
|
|
1161
|
+
const totalLength = prefix.length + visibleLabel.length;
|
|
1162
|
+
this.hitboxes.push({
|
|
1163
|
+
id,
|
|
1164
|
+
y,
|
|
1165
|
+
x1: x,
|
|
1166
|
+
x2: Math.min(x + totalLength - 1, scr.clipRight === null ? scr.cols - 2 : scr.clipRight),
|
|
1167
|
+
action,
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
return x + totalLength;
|
|
1164
1171
|
}
|
|
1165
|
-
|
|
1166
|
-
// Область взаимодействия
|
|
1167
|
-
const totalLength = (prefix ? prefix.length : 0) + visibleLabel.length;
|
|
1168
|
-
|
|
1169
|
-
this.hitboxes.push({
|
|
1170
|
-
id,
|
|
1171
|
-
y,
|
|
1172
|
-
x1: x,
|
|
1173
|
-
x2: x + totalLength - 1,
|
|
1174
|
-
action
|
|
1175
|
-
});
|
|
1176
|
-
|
|
1177
|
-
return x + totalLength;
|
|
1178
|
-
}
|
|
1179
1172
|
|
|
1180
1173
|
|
|
1181
1174
|
openPrompt(label, currentVal, onConfirm) {
|
|
@@ -1183,7 +1176,6 @@ class EvaNodeTUI {
|
|
|
1183
1176
|
this.modal = {
|
|
1184
1177
|
label,
|
|
1185
1178
|
value: val,
|
|
1186
|
-
cursor: val.length,
|
|
1187
1179
|
onConfirm,
|
|
1188
1180
|
};
|
|
1189
1181
|
this.render();
|
|
@@ -1206,6 +1198,8 @@ class EvaNodeTUI {
|
|
|
1206
1198
|
|
|
1207
1199
|
this.focusOrder = [];
|
|
1208
1200
|
this.hitboxes = [];
|
|
1201
|
+
this.queueViewport = null;
|
|
1202
|
+
this.libraryViewport = null;
|
|
1209
1203
|
|
|
1210
1204
|
if (rows < 22 || cols < 76) {
|
|
1211
1205
|
scr.putStr(1, 2, `Окно терминала слишком маленькое (${cols}x${rows}). Минимум: 78x22.`, ANSI.fgRed + ANSI.bold);
|
|
@@ -1213,7 +1207,19 @@ class EvaNodeTUI {
|
|
|
1213
1207
|
return;
|
|
1214
1208
|
}
|
|
1215
1209
|
|
|
1216
|
-
|
|
1210
|
+
this._renderHeader(scr, cols);
|
|
1211
|
+
const { x, w } = this._renderNavigation(scr, rows, cols);
|
|
1212
|
+
this._renderActiveTab(scr, x, w, rows);
|
|
1213
|
+
|
|
1214
|
+
if (this.focusOrder.length > 0 && !this.focusOrder.includes(this.focusId)) {
|
|
1215
|
+
this.focusId = this.focusOrder[0];
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
this._renderFooter(scr, cols, rows);
|
|
1219
|
+
process.stdout.write(scr.renderToAnsi());
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
_renderHeader(scr, cols) {
|
|
1217
1223
|
ASCII_LOGO.forEach((line, i) => {
|
|
1218
1224
|
scr.putStr(1 + i, 2, line, ANSI.fgWhite + ANSI.bold);
|
|
1219
1225
|
});
|
|
@@ -1224,8 +1230,9 @@ class EvaNodeTUI {
|
|
|
1224
1230
|
scr.putStr(5, cols - tagline.length - 3, tagline, ANSI.fgWhite);
|
|
1225
1231
|
scr.putStr(6, cols - tagline.length - 3, '─'.repeat(tagline.length), ANSI.fgCyan);
|
|
1226
1232
|
}
|
|
1233
|
+
}
|
|
1227
1234
|
|
|
1228
|
-
|
|
1235
|
+
_renderNavigation(scr, rows, cols) {
|
|
1229
1236
|
const sidebarW = 19;
|
|
1230
1237
|
const navItems = [
|
|
1231
1238
|
{ tab: 1, label: '≡ General' },
|
|
@@ -1258,21 +1265,20 @@ class EvaNodeTUI {
|
|
|
1258
1265
|
scr.putStr(r, sidebarW + 1, '│', ANSI.fgWhite);
|
|
1259
1266
|
}
|
|
1260
1267
|
|
|
1261
|
-
// 3. Main Content Area
|
|
1262
1268
|
const mainX = sidebarW + 3;
|
|
1263
1269
|
const mainW = cols - mainX - 2;
|
|
1270
|
+
return { x: mainX, w: mainW };
|
|
1271
|
+
}
|
|
1264
1272
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
else if (this.activeTab ===
|
|
1268
|
-
else if (this.activeTab ===
|
|
1269
|
-
else if (this.activeTab ===
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
this.focusId = this.focusOrder[0];
|
|
1273
|
-
}
|
|
1273
|
+
_renderActiveTab(scr, x, w, rows) {
|
|
1274
|
+
if (this.activeTab === 1) this._renderDownloadTab(scr, x, w, rows);
|
|
1275
|
+
else if (this.activeTab === 2) this._renderQueueTab(scr, x, w, rows);
|
|
1276
|
+
else if (this.activeTab === 3) this._renderLibraryTab(scr, x, w, rows);
|
|
1277
|
+
else if (this.activeTab === 4) this._renderSettingsTab(scr, x, w, rows);
|
|
1278
|
+
else if (this.activeTab === 5) this._renderAboutTab(scr, x, w, rows);
|
|
1279
|
+
}
|
|
1274
1280
|
|
|
1275
|
-
|
|
1281
|
+
_renderFooter(scr, cols, rows) {
|
|
1276
1282
|
if (this.modal) {
|
|
1277
1283
|
const prefix = ` * ${this.modal.label}: `;
|
|
1278
1284
|
scr.putStr(rows - 2, 0, prefix, ANSI.bgCyan + ANSI.fgBlack + ANSI.bold);
|
|
@@ -1283,7 +1289,7 @@ class EvaNodeTUI {
|
|
|
1283
1289
|
scr.putStr(rows - 1, 2, footerLeft, ANSI.fgCyan + ANSI.bold);
|
|
1284
1290
|
|
|
1285
1291
|
const items = this.manager.items;
|
|
1286
|
-
const activeItems = items.filter((
|
|
1292
|
+
const activeItems = items.filter((item) => item.status === 'Downloading');
|
|
1287
1293
|
const doneCnt = items.filter((i) => i.status === 'Done').length;
|
|
1288
1294
|
|
|
1289
1295
|
let pct = 0;
|
|
@@ -1294,22 +1300,16 @@ class EvaNodeTUI {
|
|
|
1294
1300
|
pct = 100;
|
|
1295
1301
|
}
|
|
1296
1302
|
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1303
|
+
const queueStatus = `│ ${activeItems.length}↓ ${doneCnt}✓ ${pct}%`;
|
|
1304
|
+
const queueStyle = activeItems.length > 0 ? ANSI.fgGreen + ANSI.bold : ANSI.fgWhite;
|
|
1305
|
+
const queueStartX = cols - queueStatus.length;
|
|
1306
|
+
scr.putStr(rows - 1, queueStartX, queueStatus, queueStyle);
|
|
1300
1307
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
scr.putStr(rows - 1, qStartX, queueStatusStr, qStyle);
|
|
1305
|
-
|
|
1306
|
-
const afterQueueX = qStartX + queueStatusStr.length + 2;
|
|
1307
|
-
if (this.statusBanner && afterQueueX < cols - 10) {
|
|
1308
|
-
scr.putStr(rows - 1, afterQueueX, `│ ${this.statusBanner}`.slice(0, cols - afterQueueX - 2), ANSI.fgCyan);
|
|
1308
|
+
const statusX = queueStartX + queueStatus.length + 2;
|
|
1309
|
+
if (this.statusBanner && statusX < cols - 10) {
|
|
1310
|
+
scr.putStr(rows - 1, statusX, `│ ${this.statusBanner}`.slice(0, cols - statusX - 2), ANSI.fgCyan);
|
|
1309
1311
|
}
|
|
1310
1312
|
}
|
|
1311
|
-
|
|
1312
|
-
process.stdout.write(scr.renderToAnsi());
|
|
1313
1313
|
}
|
|
1314
1314
|
|
|
1315
1315
|
// -------------------------------------------------------------------------
|
|
@@ -1318,9 +1318,11 @@ class EvaNodeTUI {
|
|
|
1318
1318
|
|
|
1319
1319
|
_renderDownloadTab(scr, x, w, rows) {
|
|
1320
1320
|
let y = 8;
|
|
1321
|
+
const previousClipRight = scr.clipRight;
|
|
1321
1322
|
|
|
1322
1323
|
// --- Box 1: Select a source (1-9) & URL input ---
|
|
1323
1324
|
scr.drawBox(y, x, 7, w, 'Select a source');
|
|
1325
|
+
scr.clipRight = x + w - 2;
|
|
1324
1326
|
const colWidth = Math.floor(w / 3);
|
|
1325
1327
|
const col1X = x + 2;
|
|
1326
1328
|
const col2X = x + colWidth;
|
|
@@ -1344,7 +1346,9 @@ class EvaNodeTUI {
|
|
|
1344
1346
|
);
|
|
1345
1347
|
});
|
|
1346
1348
|
|
|
1349
|
+
scr.clipRight = previousClipRight;
|
|
1347
1350
|
scr.putStr(y + 4, x + 0, '├' + '─'.repeat(w - 2) + '┤', ANSI.fgWhite);
|
|
1351
|
+
scr.clipRight = x + w - 2;
|
|
1348
1352
|
const srcObj = SOURCES.find((s) => s.id === this.opts.sourceId) || SOURCES[0];
|
|
1349
1353
|
const urlDisp = this.opts.url ? this.opts.url : `(нажмите 'u' или кликните для ввода URL — ${srcObj.hint})`;
|
|
1350
1354
|
this.registerWidget(
|
|
@@ -1361,7 +1365,9 @@ class EvaNodeTUI {
|
|
|
1361
1365
|
|
|
1362
1366
|
// --- Box 2: Download Options & yt-dlp Flags ---
|
|
1363
1367
|
const box2H = 11;
|
|
1368
|
+
scr.clipRight = previousClipRight;
|
|
1364
1369
|
scr.drawBox(y, x, box2H, w, 'Download Options Flags');
|
|
1370
|
+
scr.clipRight = x + w - 2;
|
|
1365
1371
|
const c1 = x + 2;
|
|
1366
1372
|
|
|
1367
1373
|
// Row 1: Audio (-x, metadata, thumbnail)
|
|
@@ -1472,43 +1478,47 @@ class EvaNodeTUI {
|
|
|
1472
1478
|
|
|
1473
1479
|
// Row 6: Naming & Archive (-o & --download-archive)
|
|
1474
1480
|
scr.putStr(y + 6, c1, 'ФАЙЛЫ:', ANSI.fgCyan + ANSI.bold);
|
|
1475
|
-
const
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1481
|
+
const rowRight = x + w - 2;
|
|
1482
|
+
const templateX = c1 + 9;
|
|
1483
|
+
const rowWidth = rowRight - templateX + 1;
|
|
1484
|
+
const templateMaxWidth = Math.max(4, 4 + Math.floor(Math.max(0, rowWidth - 24) / 2));
|
|
1485
|
+
const templateEnd = this.registerWidget(
|
|
1486
|
+
scr,
|
|
1487
|
+
'flag_otpl',
|
|
1488
|
+
y + 6,
|
|
1489
|
+
templateX,
|
|
1490
|
+
`-o "${this.opts.outputTemplate}"`,
|
|
1491
|
+
() => this._toggle('usePlaylistTemplate'),
|
|
1492
|
+
{
|
|
1493
|
+
checked: this.opts.usePlaylistTemplate,
|
|
1494
|
+
maxWidth: templateMaxWidth,
|
|
1495
|
+
}
|
|
1496
|
+
);
|
|
1497
|
+
const templateEditEnd = this.registerWidget(scr, 'edit_otpl', y + 6, templateEnd + 1, '[✎ ]', () => {
|
|
1498
|
+
return this.openPrompt('Шаблон имени (-o)', this.opts.outputTemplate, (v) => {
|
|
1499
|
+
this.opts.outputTemplate = v || '%(title)s.%(ext)s';
|
|
1500
|
+
});
|
|
1501
|
+
}, { maxWidth: 6 });
|
|
1496
1502
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
})
|
|
1503
|
+
const archiveX = templateEditEnd + 2;
|
|
1504
|
+
const archiveMaxWidth = Math.max(4, rowRight - archiveX - '[✎ ]'.length);
|
|
1505
|
+
const archiveEnd = this.registerWidget(
|
|
1506
|
+
scr,
|
|
1507
|
+
'flag_archive',
|
|
1508
|
+
y + 6,
|
|
1509
|
+
archiveX,
|
|
1510
|
+
`История загрузки ${this.opts.archiveFile}`,
|
|
1511
|
+
() => this._toggle('useDownloadArchive'),
|
|
1512
|
+
{
|
|
1513
|
+
checked: this.opts.useDownloadArchive,
|
|
1514
|
+
maxWidth: archiveMaxWidth,
|
|
1515
|
+
}
|
|
1511
1516
|
);
|
|
1517
|
+
this.registerWidget(scr, 'edit_archive', y + 6, archiveEnd + 1, '[✎ ]', () => {
|
|
1518
|
+
return this.openPrompt('Файл архива (--download-archive)', this.opts.archiveFile, (v) => {
|
|
1519
|
+
this.opts.archiveFile = v || 'archive.txt';
|
|
1520
|
+
});
|
|
1521
|
+
}, { maxWidth: 6 });
|
|
1512
1522
|
|
|
1513
1523
|
// Row 7: Playlists (--playlist-items & --playlist-reverse)
|
|
1514
1524
|
scr.putStr(y + 7, c1, 'ПЛЕЙЛИСТ:', ANSI.fgCyan + ANSI.bold);
|
|
@@ -1523,7 +1533,7 @@ class EvaNodeTUI {
|
|
|
1523
1533
|
{ checked: this.opts.usePlaylistItems }
|
|
1524
1534
|
) + 1;
|
|
1525
1535
|
px =
|
|
1526
|
-
this.registerWidget(scr, 'edit_plitems', y + 7, px, '[
|
|
1536
|
+
this.registerWidget(scr, 'edit_plitems', y + 7, px, '[✎ ]', () =>
|
|
1527
1537
|
this.openPrompt('Номера треков (--playlist-items, напр. 1-5)', this.opts.playlistItems, (v) => {
|
|
1528
1538
|
this.opts.playlistItems = v || '1-5';
|
|
1529
1539
|
})
|
|
@@ -1546,7 +1556,7 @@ class EvaNodeTUI {
|
|
|
1546
1556
|
? '…' + this.opts.outputDir.slice(-(maxDirLen - 1))
|
|
1547
1557
|
: this.opts.outputDir;
|
|
1548
1558
|
let dx =
|
|
1549
|
-
this.registerWidget(scr, 'edit_outdir', y + 9, c1,
|
|
1559
|
+
this.registerWidget(scr, 'edit_outdir', y + 9, c1, `📁: ${shortDir} [✎ ]`, () =>
|
|
1550
1560
|
this.openPrompt('Папка сохранения', this.opts.outputDir, (v) => {
|
|
1551
1561
|
this.opts.outputDir = v || path.join(os.homedir(), 'Downloads');
|
|
1552
1562
|
})
|
|
@@ -1559,6 +1569,7 @@ class EvaNodeTUI {
|
|
|
1559
1569
|
});
|
|
1560
1570
|
|
|
1561
1571
|
y += box2H;
|
|
1572
|
+
scr.clipRight = previousClipRight;
|
|
1562
1573
|
|
|
1563
1574
|
// Word-wrapped CLI Preview Lines (never overflows the window width)
|
|
1564
1575
|
const fullCliText = `CLI: ${buildCommandPreview(this.opts)}`;
|
|
@@ -1595,10 +1606,19 @@ class EvaNodeTUI {
|
|
|
1595
1606
|
this.selectedQueueItem = items[this.selectedQueueIdx] || null;
|
|
1596
1607
|
|
|
1597
1608
|
const listRows = Math.max(1, h - 6);
|
|
1609
|
+
this.queueViewport = { x, right: x + w, top: y + 3, rows: listRows };
|
|
1610
|
+
const maxScrollOffset = Math.max(0, items.length - listRows);
|
|
1611
|
+
this.queueScrollOffset = Math.max(0, Math.min(this.queueScrollOffset, maxScrollOffset));
|
|
1612
|
+
if (this.selectedQueueIdx < this.queueScrollOffset) {
|
|
1613
|
+
this.queueScrollOffset = this.selectedQueueIdx;
|
|
1614
|
+
} else if (this.selectedQueueIdx >= this.queueScrollOffset + listRows) {
|
|
1615
|
+
this.queueScrollOffset = this.selectedQueueIdx - listRows + 1;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1598
1618
|
if (items.length === 0) {
|
|
1599
1619
|
scr.putStr(y + 3, x + 4, 'Очередь пуста. Добавьте трек во вкладке [1] Download.', ANSI.fgWhite);
|
|
1600
1620
|
} else {
|
|
1601
|
-
const startI =
|
|
1621
|
+
const startI = this.queueScrollOffset;
|
|
1602
1622
|
const slice = items.slice(startI, startI + listRows);
|
|
1603
1623
|
slice.forEach((it, rowOffset) => {
|
|
1604
1624
|
const idx = startI + rowOffset;
|
|
@@ -1647,6 +1667,35 @@ class EvaNodeTUI {
|
|
|
1647
1667
|
},
|
|
1648
1668
|
});
|
|
1649
1669
|
});
|
|
1670
|
+
|
|
1671
|
+
if (items.length > listRows) {
|
|
1672
|
+
const scrollbarX = x + w - 2;
|
|
1673
|
+
const thumbSize = Math.max(1, Math.floor((listRows * listRows) / items.length));
|
|
1674
|
+
const thumbTravel = listRows - thumbSize;
|
|
1675
|
+
const thumbTop = maxScrollOffset > 0
|
|
1676
|
+
? Math.round((this.queueScrollOffset / maxScrollOffset) * thumbTravel)
|
|
1677
|
+
: 0;
|
|
1678
|
+
for (let rowOffset = 0; rowOffset < listRows; rowOffset++) {
|
|
1679
|
+
const isThumb = rowOffset >= thumbTop && rowOffset < thumbTop + thumbSize;
|
|
1680
|
+
const scrollbarY = y + 3 + rowOffset;
|
|
1681
|
+
scr.putStr(scrollbarY, scrollbarX, isThumb ? '█' : '│', isThumb ? ANSI.fgCyan : ANSI.fgGray);
|
|
1682
|
+
this.hitboxes.push({
|
|
1683
|
+
id: 'q_scroll',
|
|
1684
|
+
y: scrollbarY,
|
|
1685
|
+
x1: scrollbarX,
|
|
1686
|
+
x2: scrollbarX,
|
|
1687
|
+
action: () => {
|
|
1688
|
+
const nextThumbTop = Math.max(0, Math.min(thumbTravel, rowOffset - Math.floor(thumbSize / 2)));
|
|
1689
|
+
this.queueScrollOffset = thumbTravel > 0
|
|
1690
|
+
? Math.round((nextThumbTop / thumbTravel) * maxScrollOffset)
|
|
1691
|
+
: 0;
|
|
1692
|
+
this.selectedQueueIdx = this.queueScrollOffset;
|
|
1693
|
+
this.selectedQueueItem = items[this.selectedQueueIdx] || null;
|
|
1694
|
+
this.render();
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1650
1699
|
}
|
|
1651
1700
|
|
|
1652
1701
|
const ctrlY = y + h - 2;
|
|
@@ -1687,7 +1736,7 @@ class EvaNodeTUI {
|
|
|
1687
1736
|
_renderLibraryTab(scr, x, w, rows) {
|
|
1688
1737
|
const h = rows - 9;
|
|
1689
1738
|
scr.drawBox(8, x, h, w, `Downloaded Media Library (${this.opts.outputDir})`);
|
|
1690
|
-
const outDir = this.opts.outputDir
|
|
1739
|
+
const outDir = resolveOutputDir(this.opts.outputDir);
|
|
1691
1740
|
const exts = new Set(['.mp3', '.m4a', '.flac', '.wav', '.mp4', '.mkv', '.webm', '.opus', '.ogg']);
|
|
1692
1741
|
let files = [];
|
|
1693
1742
|
try {
|
|
@@ -1708,28 +1757,105 @@ class EvaNodeTUI {
|
|
|
1708
1757
|
const numberColW = 4;
|
|
1709
1758
|
const formatColW = 7;
|
|
1710
1759
|
const sizeColW = 10;
|
|
1711
|
-
const nameColW = Math.
|
|
1760
|
+
const nameColW = Math.max(1, contentWidth - numberColW - formatColW - sizeColW - 5);
|
|
1712
1761
|
const formatX = numberColW + 1 + nameColW + 1;
|
|
1713
1762
|
const sizeX = formatX + formatColW + 1;
|
|
1763
|
+
const listTop = 11;
|
|
1764
|
+
const listRows = Math.max(1, h - 4);
|
|
1765
|
+
const maxScrollOffset = Math.max(0, files.length - listRows);
|
|
1766
|
+
this.libraryScrollOffset = Math.max(0, Math.min(this.libraryScrollOffset, maxScrollOffset));
|
|
1767
|
+
this.libraryViewport = { x, right: x + w, top: listTop, rows: listRows, count: files.length };
|
|
1768
|
+
scr.putStr(10, x, '├' + '─'.repeat(w - 2) + '┤', ANSI.fgWhite);
|
|
1714
1769
|
if (files.length === 0) {
|
|
1715
|
-
scr.putStr(
|
|
1770
|
+
scr.putStr(listTop, x + 3, `No media files found in ${outDir}.`.slice(0, w - 6), ANSI.fgWhite);
|
|
1716
1771
|
} else {
|
|
1717
|
-
scr.putStr(
|
|
1718
|
-
scr.putStr(
|
|
1719
|
-
scr.putStr(
|
|
1720
|
-
scr.putStr(
|
|
1721
|
-
|
|
1722
|
-
|
|
1772
|
+
scr.putStr(9, x + 3, '#', ANSI.fgCyan + ANSI.bold);
|
|
1773
|
+
scr.putStr(9, x + 3 + numberColW + 1, 'Title', ANSI.fgCyan + ANSI.bold);
|
|
1774
|
+
scr.putStr(9, x + 3 + formatX, 'Format', ANSI.fgCyan + ANSI.bold);
|
|
1775
|
+
scr.putStr(9, x + 3 + sizeX, 'Size', ANSI.fgCyan + ANSI.bold);
|
|
1776
|
+
files.slice(this.libraryScrollOffset, this.libraryScrollOffset + listRows).forEach((f, rowOffset) => {
|
|
1777
|
+
const fileIndex = this.libraryScrollOffset + rowOffset;
|
|
1723
1778
|
const mb = formatBytesToMB(f.stat.size);
|
|
1724
|
-
const row =
|
|
1725
|
-
const filename = f.name
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
scr.putStr(row, x +
|
|
1729
|
-
scr.putStr(row, x + 3
|
|
1730
|
-
scr.
|
|
1731
|
-
scr.
|
|
1779
|
+
const row = listTop + rowOffset;
|
|
1780
|
+
const filename = fitTextToCellWidth(f.name, nameColW);
|
|
1781
|
+
const isSelected = f.full === this.selectedLibraryFile;
|
|
1782
|
+
const rowStyle = isSelected ? ANSI.bgWhite + ANSI.fgBlack + ANSI.bold : ANSI.fgWhite;
|
|
1783
|
+
if (isSelected) scr.putStr(row, x + 2, ' '.repeat(w - 4), rowStyle);
|
|
1784
|
+
scr.putStr(row, x + 3, String(fileIndex + 1).padEnd(numberColW), rowStyle);
|
|
1785
|
+
scr.putStrWithin(row, x + 3 + numberColW + 1, filename, nameColW, rowStyle);
|
|
1786
|
+
scr.putStrWithin(row, x + 3 + formatX, f.ext.slice(1, formatColW + 1).padEnd(formatColW), formatColW, rowStyle);
|
|
1787
|
+
scr.putStrWithin(row, x + 3 + sizeX, mb.slice(0, sizeColW).padStart(sizeColW), sizeColW, rowStyle);
|
|
1788
|
+
this.hitboxes.push({
|
|
1789
|
+
id: `library_file_${fileIndex}`,
|
|
1790
|
+
y: row,
|
|
1791
|
+
x1: x + 2,
|
|
1792
|
+
x2: x + w - 3,
|
|
1793
|
+
action: () => this._selectLibraryFile(f),
|
|
1794
|
+
});
|
|
1732
1795
|
});
|
|
1796
|
+
|
|
1797
|
+
if (files.length > listRows) {
|
|
1798
|
+
const scrollbarX = x + w - 2;
|
|
1799
|
+
const thumbSize = Math.max(1, Math.floor((listRows * listRows) / files.length));
|
|
1800
|
+
const thumbTravel = listRows - thumbSize;
|
|
1801
|
+
const thumbTop = maxScrollOffset > 0
|
|
1802
|
+
? Math.round((this.libraryScrollOffset / maxScrollOffset) * thumbTravel)
|
|
1803
|
+
: 0;
|
|
1804
|
+
for (let rowOffset = 0; rowOffset < listRows; rowOffset++) {
|
|
1805
|
+
const isThumb = rowOffset >= thumbTop && rowOffset < thumbTop + thumbSize;
|
|
1806
|
+
const scrollbarY = listTop + rowOffset;
|
|
1807
|
+
scr.putStr(scrollbarY, scrollbarX, isThumb ? '█' : '│', isThumb ? ANSI.fgCyan : ANSI.fgGray);
|
|
1808
|
+
this.hitboxes.push({
|
|
1809
|
+
id: 'library_scroll',
|
|
1810
|
+
y: scrollbarY,
|
|
1811
|
+
x1: scrollbarX,
|
|
1812
|
+
x2: scrollbarX,
|
|
1813
|
+
action: () => {
|
|
1814
|
+
const nextThumbTop = Math.max(0, Math.min(thumbTravel, rowOffset - Math.floor(thumbSize / 2)));
|
|
1815
|
+
this.libraryScrollOffset = thumbTravel > 0
|
|
1816
|
+
? Math.round((nextThumbTop / thumbTravel) * maxScrollOffset)
|
|
1817
|
+
: 0;
|
|
1818
|
+
this.render();
|
|
1819
|
+
},
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
_selectLibraryFile(file) {
|
|
1827
|
+
const now = Date.now();
|
|
1828
|
+
const isDoubleClick =
|
|
1829
|
+
this.lastLibraryClick &&
|
|
1830
|
+
this.lastLibraryClick.path === file.full &&
|
|
1831
|
+
now - this.lastLibraryClick.time <= LIBRARY_DOUBLE_CLICK_MS;
|
|
1832
|
+
|
|
1833
|
+
this.selectedLibraryFile = file.full;
|
|
1834
|
+
this.lastLibraryClick = isDoubleClick ? null : { path: file.full, time: now };
|
|
1835
|
+
if (isDoubleClick) {
|
|
1836
|
+
this._openLibraryFile(file.full);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
this.render();
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
_openLibraryFile(filePath) {
|
|
1843
|
+
const opener = process.platform === 'win32'
|
|
1844
|
+
? 'explorer.exe'
|
|
1845
|
+
: process.platform === 'darwin'
|
|
1846
|
+
? 'open'
|
|
1847
|
+
: 'xdg-open';
|
|
1848
|
+
|
|
1849
|
+
try {
|
|
1850
|
+
const child = spawn(opener, [filePath], { detached: true, stdio: 'ignore' });
|
|
1851
|
+
child.once('error', (error) => {
|
|
1852
|
+
this.statusBanner = `Не удалось открыть файл: ${error.message}`;
|
|
1853
|
+
this.render();
|
|
1854
|
+
});
|
|
1855
|
+
child.unref();
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
this.statusBanner = `Не удалось открыть файл: ${error.message}`;
|
|
1858
|
+
this.render();
|
|
1733
1859
|
}
|
|
1734
1860
|
}
|
|
1735
1861
|
|
|
@@ -1763,11 +1889,11 @@ class EvaNodeTUI {
|
|
|
1763
1889
|
() => this._toggle('useProxy'),
|
|
1764
1890
|
{ checked: this.opts.useProxy }
|
|
1765
1891
|
) + 2;
|
|
1766
|
-
this.registerWidget(scr, 'edit_proxy', 12, px, '[изменить прокси]', () =>
|
|
1767
|
-
this.openPrompt('Прокси (--proxy)', this.opts.proxyUrl, (v) => {
|
|
1892
|
+
this.registerWidget(scr, 'edit_proxy', 12, px, '[изменить прокси]', () => {
|
|
1893
|
+
return this.openPrompt('Прокси (--proxy)', this.opts.proxyUrl, (v) => {
|
|
1768
1894
|
this.opts.proxyUrl = v || 'http://127.0.0.1:8080';
|
|
1769
|
-
})
|
|
1770
|
-
);
|
|
1895
|
+
});
|
|
1896
|
+
}, { isCta: true });
|
|
1771
1897
|
|
|
1772
1898
|
let ckx =
|
|
1773
1899
|
this.registerWidget(
|
|
@@ -1786,7 +1912,7 @@ class EvaNodeTUI {
|
|
|
1786
1912
|
this.opts.useCookiesBrowser = nextB !== 'none';
|
|
1787
1913
|
this.statusBanner = `Cookies браузер: ${nextB}`;
|
|
1788
1914
|
this.render();
|
|
1789
|
-
});
|
|
1915
|
+
}, { isCta: true });
|
|
1790
1916
|
|
|
1791
1917
|
scr.putStr(15, c1, 'ШАБЛОНЫ ИМЕНОВАНИЯ ФАЙЛОВ (-o):', ANSI.fgCyan + ANSI.bold);
|
|
1792
1918
|
NAMING_PRESETS.forEach((p, idx) => {
|
|
@@ -1965,7 +2091,6 @@ class EvaNodeTUI {
|
|
|
1965
2091
|
}
|
|
1966
2092
|
if (data === '\x15') {
|
|
1967
2093
|
this.modal.value = '';
|
|
1968
|
-
this.modal.cursor = 0;
|
|
1969
2094
|
this.render();
|
|
1970
2095
|
return;
|
|
1971
2096
|
}
|
|
@@ -1990,12 +2115,30 @@ class EvaNodeTUI {
|
|
|
1990
2115
|
const mx = parseInt(mouseMatch[2], 10) - 1;
|
|
1991
2116
|
const my = parseInt(mouseMatch[3], 10) - 1;
|
|
1992
2117
|
const isPress = mouseMatch[4] === 'M';
|
|
2118
|
+
if ((this.activeTab === 2 || this.activeTab === 3) && (btn === 64 || btn === 65)) {
|
|
2119
|
+
const isQueue = this.activeTab === 2;
|
|
2120
|
+
const viewport = isQueue ? this.queueViewport : this.libraryViewport;
|
|
2121
|
+
if (viewport && mx >= viewport.x && mx < viewport.right &&
|
|
2122
|
+
my >= viewport.top && my < viewport.top + viewport.rows) {
|
|
2123
|
+
if (isQueue) {
|
|
2124
|
+
const maxOffset = Math.max(0, this.manager.items.length - viewport.rows);
|
|
2125
|
+
this.queueScrollOffset = Math.max(0, Math.min(maxOffset, this.queueScrollOffset + (btn === 64 ? -1 : 1)));
|
|
2126
|
+
this.selectedQueueIdx = this.queueScrollOffset;
|
|
2127
|
+
this.selectedQueueItem = this.manager.items[this.selectedQueueIdx] || null;
|
|
2128
|
+
} else {
|
|
2129
|
+
const maxOffset = Math.max(0, viewport.count - viewport.rows);
|
|
2130
|
+
this.libraryScrollOffset = Math.max(0, Math.min(maxOffset, this.libraryScrollOffset + (btn === 64 ? -1 : 1)));
|
|
2131
|
+
}
|
|
2132
|
+
this.render();
|
|
2133
|
+
}
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
1993
2136
|
if (isPress && btn === 0) {
|
|
1994
2137
|
const hit = this.hitboxes.find((h) =>
|
|
1995
2138
|
mx >= h.x1 && mx <= h.x2 && (h.y === my || (h.nav && Math.abs(h.y - my) === 1))
|
|
1996
2139
|
);
|
|
1997
2140
|
if (hit) {
|
|
1998
|
-
this.focusId = hit.id;
|
|
2141
|
+
if (hit.id !== 'q_scroll' && hit.id !== 'library_scroll') this.focusId = hit.id;
|
|
1999
2142
|
hit.action();
|
|
2000
2143
|
}
|
|
2001
2144
|
}
|
|
@@ -2142,6 +2285,7 @@ module.exports = {
|
|
|
2142
2285
|
detectSource,
|
|
2143
2286
|
isYouTubePlaylistUrl,
|
|
2144
2287
|
playlistEntryUrl,
|
|
2288
|
+
ScreenBuffer,
|
|
2145
2289
|
DownloadManager,
|
|
2146
2290
|
formatBytesToMB,
|
|
2147
2291
|
fitTextToCellWidth,
|