eva-md 0.5.1 → 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 +125 -34
- 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 = [
|
|
@@ -1059,6 +1060,10 @@ class EvaNodeTUI {
|
|
|
1059
1060
|
this.selectedQueueItem = null;
|
|
1060
1061
|
this.queueScrollOffset = 0;
|
|
1061
1062
|
this.queueViewport = null;
|
|
1063
|
+
this.libraryScrollOffset = 0;
|
|
1064
|
+
this.libraryViewport = null;
|
|
1065
|
+
this.selectedLibraryFile = null;
|
|
1066
|
+
this.lastLibraryClick = null;
|
|
1062
1067
|
this.statusBanner = '';
|
|
1063
1068
|
|
|
1064
1069
|
this.modal = null;
|
|
@@ -1103,7 +1108,7 @@ class EvaNodeTUI {
|
|
|
1103
1108
|
? ANSI.bgGray + ANSI.fgWhite + ANSI.bold
|
|
1104
1109
|
: ANSI.bgWhite + ANSI.fgBlack + ANSI.bold;
|
|
1105
1110
|
} else if (isCta) {
|
|
1106
|
-
style =
|
|
1111
|
+
style = ['url_input', 'edit_proxy', 'cycle_browser'].includes(id)
|
|
1107
1112
|
? ANSI.fgWhite + ANSI.bold
|
|
1108
1113
|
: id === 'btn_add_queue'
|
|
1109
1114
|
? ANSI.fgGreen + ANSI.bold
|
|
@@ -1129,7 +1134,7 @@ class EvaNodeTUI {
|
|
|
1129
1134
|
}
|
|
1130
1135
|
|
|
1131
1136
|
const textX = x + prefix.length;
|
|
1132
|
-
const editMarker = '[
|
|
1137
|
+
const editMarker = '[✎ ]';
|
|
1133
1138
|
const editMarkerIndex = visibleLabel.lastIndexOf(editMarker);
|
|
1134
1139
|
if (!isFocused && editMarkerIndex >= 0) {
|
|
1135
1140
|
const folderLabel = 'Папка:';
|
|
@@ -1194,6 +1199,7 @@ class EvaNodeTUI {
|
|
|
1194
1199
|
this.focusOrder = [];
|
|
1195
1200
|
this.hitboxes = [];
|
|
1196
1201
|
this.queueViewport = null;
|
|
1202
|
+
this.libraryViewport = null;
|
|
1197
1203
|
|
|
1198
1204
|
if (rows < 22 || cols < 76) {
|
|
1199
1205
|
scr.putStr(1, 2, `Окно терминала слишком маленькое (${cols}x${rows}). Минимум: 78x22.`, ANSI.fgRed + ANSI.bold);
|
|
@@ -1488,14 +1494,14 @@ class EvaNodeTUI {
|
|
|
1488
1494
|
maxWidth: templateMaxWidth,
|
|
1489
1495
|
}
|
|
1490
1496
|
);
|
|
1491
|
-
const templateEditEnd = this.registerWidget(scr, 'edit_otpl', y + 6, templateEnd + 1, '[
|
|
1497
|
+
const templateEditEnd = this.registerWidget(scr, 'edit_otpl', y + 6, templateEnd + 1, '[✎ ]', () => {
|
|
1492
1498
|
return this.openPrompt('Шаблон имени (-o)', this.opts.outputTemplate, (v) => {
|
|
1493
1499
|
this.opts.outputTemplate = v || '%(title)s.%(ext)s';
|
|
1494
1500
|
});
|
|
1495
1501
|
}, { maxWidth: 6 });
|
|
1496
1502
|
|
|
1497
1503
|
const archiveX = templateEditEnd + 2;
|
|
1498
|
-
const archiveMaxWidth = Math.max(4, rowRight - archiveX - '[
|
|
1504
|
+
const archiveMaxWidth = Math.max(4, rowRight - archiveX - '[✎ ]'.length);
|
|
1499
1505
|
const archiveEnd = this.registerWidget(
|
|
1500
1506
|
scr,
|
|
1501
1507
|
'flag_archive',
|
|
@@ -1508,7 +1514,7 @@ class EvaNodeTUI {
|
|
|
1508
1514
|
maxWidth: archiveMaxWidth,
|
|
1509
1515
|
}
|
|
1510
1516
|
);
|
|
1511
|
-
this.registerWidget(scr, 'edit_archive', y + 6, archiveEnd + 1, '[
|
|
1517
|
+
this.registerWidget(scr, 'edit_archive', y + 6, archiveEnd + 1, '[✎ ]', () => {
|
|
1512
1518
|
return this.openPrompt('Файл архива (--download-archive)', this.opts.archiveFile, (v) => {
|
|
1513
1519
|
this.opts.archiveFile = v || 'archive.txt';
|
|
1514
1520
|
});
|
|
@@ -1527,7 +1533,7 @@ class EvaNodeTUI {
|
|
|
1527
1533
|
{ checked: this.opts.usePlaylistItems }
|
|
1528
1534
|
) + 1;
|
|
1529
1535
|
px =
|
|
1530
|
-
this.registerWidget(scr, 'edit_plitems', y + 7, px, '[
|
|
1536
|
+
this.registerWidget(scr, 'edit_plitems', y + 7, px, '[✎ ]', () =>
|
|
1531
1537
|
this.openPrompt('Номера треков (--playlist-items, напр. 1-5)', this.opts.playlistItems, (v) => {
|
|
1532
1538
|
this.opts.playlistItems = v || '1-5';
|
|
1533
1539
|
})
|
|
@@ -1550,7 +1556,7 @@ class EvaNodeTUI {
|
|
|
1550
1556
|
? '…' + this.opts.outputDir.slice(-(maxDirLen - 1))
|
|
1551
1557
|
: this.opts.outputDir;
|
|
1552
1558
|
let dx =
|
|
1553
|
-
this.registerWidget(scr, 'edit_outdir', y + 9, c1,
|
|
1559
|
+
this.registerWidget(scr, 'edit_outdir', y + 9, c1, `📁: ${shortDir} [✎ ]`, () =>
|
|
1554
1560
|
this.openPrompt('Папка сохранения', this.opts.outputDir, (v) => {
|
|
1555
1561
|
this.opts.outputDir = v || path.join(os.homedir(), 'Downloads');
|
|
1556
1562
|
})
|
|
@@ -1754,23 +1760,102 @@ class EvaNodeTUI {
|
|
|
1754
1760
|
const nameColW = Math.max(1, contentWidth - numberColW - formatColW - sizeColW - 5);
|
|
1755
1761
|
const formatX = numberColW + 1 + nameColW + 1;
|
|
1756
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);
|
|
1757
1769
|
if (files.length === 0) {
|
|
1758
|
-
scr.putStr(
|
|
1770
|
+
scr.putStr(listTop, x + 3, `No media files found in ${outDir}.`.slice(0, w - 6), ANSI.fgWhite);
|
|
1759
1771
|
} else {
|
|
1760
|
-
scr.putStr(
|
|
1761
|
-
scr.putStr(
|
|
1762
|
-
scr.putStr(
|
|
1763
|
-
scr.putStr(
|
|
1764
|
-
|
|
1765
|
-
|
|
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;
|
|
1766
1778
|
const mb = formatBytesToMB(f.stat.size);
|
|
1767
|
-
const row =
|
|
1779
|
+
const row = listTop + rowOffset;
|
|
1768
1780
|
const filename = fitTextToCellWidth(f.name, nameColW);
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
scr.
|
|
1772
|
-
scr.
|
|
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
|
+
});
|
|
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();
|
|
1773
1854
|
});
|
|
1855
|
+
child.unref();
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
this.statusBanner = `Не удалось открыть файл: ${error.message}`;
|
|
1858
|
+
this.render();
|
|
1774
1859
|
}
|
|
1775
1860
|
}
|
|
1776
1861
|
|
|
@@ -1804,11 +1889,11 @@ class EvaNodeTUI {
|
|
|
1804
1889
|
() => this._toggle('useProxy'),
|
|
1805
1890
|
{ checked: this.opts.useProxy }
|
|
1806
1891
|
) + 2;
|
|
1807
|
-
this.registerWidget(scr, 'edit_proxy', 12, px, '[изменить прокси]', () =>
|
|
1808
|
-
this.openPrompt('Прокси (--proxy)', this.opts.proxyUrl, (v) => {
|
|
1892
|
+
this.registerWidget(scr, 'edit_proxy', 12, px, '[изменить прокси]', () => {
|
|
1893
|
+
return this.openPrompt('Прокси (--proxy)', this.opts.proxyUrl, (v) => {
|
|
1809
1894
|
this.opts.proxyUrl = v || 'http://127.0.0.1:8080';
|
|
1810
|
-
})
|
|
1811
|
-
);
|
|
1895
|
+
});
|
|
1896
|
+
}, { isCta: true });
|
|
1812
1897
|
|
|
1813
1898
|
let ckx =
|
|
1814
1899
|
this.registerWidget(
|
|
@@ -1827,7 +1912,7 @@ class EvaNodeTUI {
|
|
|
1827
1912
|
this.opts.useCookiesBrowser = nextB !== 'none';
|
|
1828
1913
|
this.statusBanner = `Cookies браузер: ${nextB}`;
|
|
1829
1914
|
this.render();
|
|
1830
|
-
});
|
|
1915
|
+
}, { isCta: true });
|
|
1831
1916
|
|
|
1832
1917
|
scr.putStr(15, c1, 'ШАБЛОНЫ ИМЕНОВАНИЯ ФАЙЛОВ (-o):', ANSI.fgCyan + ANSI.bold);
|
|
1833
1918
|
NAMING_PRESETS.forEach((p, idx) => {
|
|
@@ -2030,14 +2115,20 @@ class EvaNodeTUI {
|
|
|
2030
2115
|
const mx = parseInt(mouseMatch[2], 10) - 1;
|
|
2031
2116
|
const my = parseInt(mouseMatch[3], 10) - 1;
|
|
2032
2117
|
const isPress = mouseMatch[4] === 'M';
|
|
2033
|
-
if (this.activeTab === 2 && (btn === 64 || btn === 65)) {
|
|
2034
|
-
const
|
|
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;
|
|
2035
2121
|
if (viewport && mx >= viewport.x && mx < viewport.right &&
|
|
2036
2122
|
my >= viewport.top && my < viewport.top + viewport.rows) {
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
+
}
|
|
2041
2132
|
this.render();
|
|
2042
2133
|
}
|
|
2043
2134
|
return;
|
|
@@ -2047,7 +2138,7 @@ class EvaNodeTUI {
|
|
|
2047
2138
|
mx >= h.x1 && mx <= h.x2 && (h.y === my || (h.nav && Math.abs(h.y - my) === 1))
|
|
2048
2139
|
);
|
|
2049
2140
|
if (hit) {
|
|
2050
|
-
if (hit.id !== 'q_scroll') this.focusId = hit.id;
|
|
2141
|
+
if (hit.id !== 'q_scroll' && hit.id !== 'library_scroll') this.focusId = hit.id;
|
|
2051
2142
|
hit.action();
|
|
2052
2143
|
}
|
|
2053
2144
|
}
|