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.
Files changed (2) hide show
  1. package/eva.js +125 -34
  2. 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.5.1 — EVA Media Downloader by .maxon
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.5.1 (Node.js)';
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: 'Автор - Название: Автор - Название.mp4(формат)', tpl: '%(uploader)s - %(title)s.%(ext)s' },
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 = id === 'url_input'
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 - '[ред.]'.length);
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, `Папка: ${shortDir} [ред.]`, () =>
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(10, x + 3, `В папке ${outDir} пока нет скачанных медиафайлов.`.slice(0, w - 6), ANSI.fgWhite);
1770
+ scr.putStr(listTop, x + 3, `No media files found in ${outDir}.`.slice(0, w - 6), ANSI.fgWhite);
1759
1771
  } else {
1760
- scr.putStr(10, x + 3, '#', ANSI.fgCyan + ANSI.bold);
1761
- scr.putStr(10, x + 3 + numberColW + 1, 'Имя файла', ANSI.fgCyan + ANSI.bold);
1762
- scr.putStr(10, x + 3 + formatX, 'Формат', ANSI.fgCyan + ANSI.bold);
1763
- scr.putStr(10, x + 3 + sizeX, 'Размер', ANSI.fgCyan + ANSI.bold);
1764
- const visibleFileCount = Math.max(0, Math.min(files.length, rows - 15));
1765
- files.slice(0, visibleFileCount).forEach((f, i) => {
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 = 12 + i;
1779
+ const row = listTop + rowOffset;
1768
1780
  const filename = fitTextToCellWidth(f.name, nameColW);
1769
- scr.putStr(row, x + 3, String(i + 1).padEnd(numberColW), ANSI.fgWhite);
1770
- scr.putStrWithin(row, x + 3 + numberColW + 1, filename, nameColW, ANSI.fgWhite);
1771
- scr.putStrWithin(row, x + 3 + formatX, f.ext.slice(1, formatColW + 1).padEnd(formatColW), formatColW, ANSI.fgWhite);
1772
- scr.putStrWithin(row, x + 3 + sizeX, mb.slice(0, sizeColW).padStart(sizeColW), sizeColW, ANSI.fgWhite);
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 viewport = this.queueViewport;
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
- const maxOffset = Math.max(0, this.manager.items.length - viewport.rows);
2038
- this.queueScrollOffset = Math.max(0, Math.min(maxOffset, this.queueScrollOffset + (btn === 64 ? -1 : 1)));
2039
- this.selectedQueueIdx = this.queueScrollOffset;
2040
- this.selectedQueueItem = this.manager.items[this.selectedQueueIdx] || null;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eva-md",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "EVA Media Downloader CLI",
5
5
  "main": "eva.js",
6
6
  "bin": {