@juen, post #14

/*
* aminet_browser.js - Aminet Archive Browser v1.2
*
* Browse and download files from http://aminet.net
*
* - Cycle gadget : select main category
* - Listview (top) : subcategories (hardcoded)
* - Listview (middle) : file list from aminet.net
* - Double-click : same row twice = Download
* - Download button : ASL requester + http.get
* - Readme button : fetch .readme, show in window
* - Search : query aminet.net/search
* - Close window / Esc : close
*
* Run: NodeAmiga examples/aminet_browser.js
* Req: bsdsocket.library (TCP/IP stack)
*/
var gui = require('gui');
var gfx = gui.gfx;
var http = require('http');
var fs = require('fs');
/* ================================================================== */
/* Constants */
/* ================================================================== */
var AMINET = 'http://aminet.net';
var G_SUBCATS = {
biz: ['dbase', 'misc', 'spread'],
comm: ['ambos', 'amiex', 'bbs', 'cnet', 'dlg', 'fido', 'irc',
'mail', 'maxs', 'mebbs', 'misc', 'mmgr', 'net', 'news',
'tcp', 'term', 'thor', 'ums', 'uucp', 'www', 'xeno', 'yam'],
demo: ['40k', 'aga', 'bse', 'disk', 'ecs', 'euro', 'file', 'funet',
'intro', 'mag', 'mega', 'misc', 'slide', 'sound', 'sp96',
'ta95', 'ta96', 'ta97', 'tg93', 'tg95', 'tg96', 'tg97',
'tp92', 'tp94', 'tp95', 'tp96', 'track'],
dev: ['amos', 'asm', 'basic', 'blitz', 'c', 'cross', 'debug', 'e',
'gcc', 'gg', 'gui', 'hwood', 'lang', 'lib', 'm2', 'misc',
'moni', 'mui', 'obero', 'src'],
docs: ['anno', 'hard', 'help', 'hyper', 'lists', 'misc', 'rview'],
game: ['2play', 'actio', 'board', 'data', 'demo', 'edit', 'gag',
'hint', 'jump', 'misc', 'patch', 'race', 'role', 'shoot',
'strat', 'text', 'think', 'wb'],
gfx: ['3d', 'arteff', 'conv', 'edit', 'fract', 'ifx', 'misc',
'pbm', 'ppaint', 'show'],
misc: ['amag', 'antiq', 'edu', 'emu', 'fish', 'imdb', 'kids',
'math', 'misc', 'os', 'sci', 'unix', 'x11'],
mods: ['32bit', '4mat', '8voic', 'airon', 'alpma', 'anakir', 'atmos',
'aweso', 'bhead', 'bladr', 'blasa', 'blk', 'blkha', 'boing',
'boray', 'casef', 'cato', 'cels', 'chart', 'chip', 'chrom',
'chryl', 'clawz', 'cloud', 'crash', 'crn', 'csm', 'ctp',
'cust', 'cutc', 'darde', 'dean', 'delor', 'demo', 'dizzy',
'dlusn', 'doh', 'dremr', 'dsx', 'elbie', 'emax', 'ephnx',
'essen', 'evrim', 'exprt', 'facet', 'fby', 'fermx', 'fox2',
'funet', 'funk', 'grogo', 'hardc', 'hje', 'house', 'huezo',
'hw', 'ijont', 'inst', 'instr', 'jazzc', 'jogei', 'jorma',
'jungl', 'k4k', 'kaa', 'kicko', 'lead', 'mark', 'maxym',
'med', 'melod', 'melom', 'midi', 'misc', 'mnc', 'moods',
'mpg', 'mryo', 'mvp', 'ncase', 'neuro', 'nkn', 'nork',
'ooze', 'otis', 'panik', 'pdy', 'pete', 'piano', 'pop',
'pro', 'przk', 'punn', 'purg', 'racoo', 'rated', 'rock',
'roz', 's3m', 'sbc', 'sets', 'sidew', 'slc', 'slow', 'smg',
'smpl', 'sonor', 'spark', 'stame', 'stun', 'symph', 'synth',
'techn', 'toady', 'tp96', 'tranc', 'uns', 'uprgh', 'vdo',
'voice', 'voy', 'wmr', 'xceed', 'xm'],
mus: ['edit', 'midi', 'misc', 'play'],
pix: ['3dani', '3dobj', 'addic', 'anim', 'art', 'astro', 'back',
'bill', 'boot', 'clip', 'eps', 'eric', 'fauna', 'fract',
'gicon', 'glenn', 'guard', 'heiko', 'henz', 'icon', 'illu',
'imagi', 'irc', 'jake', 'jason', 'map', 'mark', 'misc',
'mpg', 'mwb', 'nevil', 'nicon', 'park', 'picon', 'real3',
'sport', 'textu', 'theme', 'tp96', 'trace', 'unrl', 'vehic',
'views', 'wb'],
text: ['bfont', 'dtp', 'edit', 'font', 'fwrit', 'hyper', 'ifont',
'misc', 'pfont', 'print', 'show', 'tex', 'tfont'],
util: ['app', 'arc', 'batch', 'blank', 'boot', 'cdity', 'cli',
'conv', 'crypt', 'dir', 'dopus', 'dtype', 'libs', 'misc',
'moni', 'mouse', 'pack', 'rexx', 'shell', 'sys', 'time',
'virus', 'wb']
};
var G_CATS = Object.keys(G_SUBCATS);
/* ================================================================== */
/* State */
/* ================================================================== */
var g_catIdx = 0; /* selected index in G_CATS */
var g_subcats = []; /* current subcategory name strings */
var g_subcatIdx = -1; /* selected subcategory listview index */
var g_files = []; /* [{name, size, date, desc}, ...] */
var g_selFile = -1; /* index of selected file in g_files */
var g_lastCode = -1; /* previous fileList evt.code (dbl-clk) */
var g_sortBy = 0; /* 0=name, 1=size, 2=date */
var g_sortDir = 0; /* 0=ascending, 1=descending */
var readmeWin = null; /* readme viewer window (or null) */
var g_rmLines = []; /* original readme lines (for hscroll) */
var g_rmScroll = 0; /* horizontal scroll offset (chars) */
var g_rmMaxLen = 0; /* longest line length (scroller total) */
/* ================================================================== */
/* Layout */
/* ================================================================== */
/* Get screen font metrics for proper layout */
var scr = gui.screenInfo();
var fw = scr.fontWidth; /* exact char width in pixels */
var fh = scr.fontHeight; /* char height in pixels */
var gadgets = [];
var gid = 1;
var rh = fh + 4; /* row height: font + padding */
var sp = 4; /* row spacing px */
var y = sp;
/* Row 1: Category / Sort / Order cycles */
gadgets.push({ kind: 'cycle', id: gid++,
left: '1%', top: y, width: '50%', height: rh,
items: G_CATS, value: 0 });
var ID_CAT = 1;
gadgets.push({ kind: 'cycle', id: gid++,
left: '53%', top: y, width: '25%', height: rh,
items: ['name', 'size', 'date'], value: 0 });
var ID_SORT = 2;
gadgets.push({ kind: 'cycle', id: gid++,
left: '80%', top: y, width: '19%', height: rh,
items: ['asc', 'desc'], value: 0 });
var ID_ORDER = 3;
y += rh + sp;
/* Bottom area: status + search + buttons = rh*3 + sp*3 */
var btmH = rh * 3 + sp * 3;
var winH = rh * 16 + btmH + sp;
var listH = winH - y - btmH - 2;
/* Subcategory listview — 6 chars + scrollbar (16px) + borders (8px) */
var subW = fw * 6 + 24;
gadgets.push({ kind: 'listview', id: gid++,
left: 4, top: y, width: subW, height: listH, flex: true,
items: [], value: 0 });
var ID_SUBCAT = 4;
/* File listview — fills remaining width */
var fileL = subW + 8;
gadgets.push({ kind: 'listview', id: gid++,
left: fileL, top: y, width: -(fileL + 4), height: listH, flex: true,
items: ['Select a subcategory...'], value: 0 });
var ID_FILES = 5;
/* Status text — top of bottom area */
gadgets.push({ kind: 'text', id: gid++,
left: '1%', top: -(rh * 3 + sp * 3), width: '98%', height: rh,
value: 'Ready -- pick a category.' });
var ID_STATUS = 6;
/* Search string — middle row of bottom area */
var searchY = -(rh * 2 + sp * 2);
gadgets.push({ kind: 'string', id: gid++,
label: 'Find:',
left: '12%', top: searchY, width: '87%', height: rh,
value: '' });
var ID_SEARCH_STR = 7;
/* Bottom row: buttons */
var btnY = -(rh + sp);
gadgets.push({ kind: 'button', id: gid++, label: 'Download',
left: '1%', top: btnY, width: '32%', height: rh });
var ID_DOWNLOAD = 8;
gadgets.push({ kind: 'button', id: gid++, label: 'Readme',
left: '34%', top: btnY, width: '32%', height: rh });
var ID_README = 9;
gadgets.push({ kind: 'button', id: gid++,
label: 'Search All',
left: '67%', top: btnY, width: '32%', height: rh });
var ID_SEARCH = 10;
/* ================================================================== */
/* Create window */
/* ================================================================== */
var win = gui.createWindow({
title: 'Aminet Browser 1.2 - NodeAmiga by Juen',
width: 520,
height: winH,
left: 20,
top: 15,
resizable: true,
minWidth: 300,
minHeight: 140,
gadgets: gadgets
});
/* Download and Readme start disabled (no file selected) */
gui.setDisabled(win, ID_DOWNLOAD, true);
gui.setDisabled(win, ID_README, true);
/* ================================================================== */
/* Utility helpers */
/* ================================================================== */
function setStatus(msg) {
gui.set(win, ID_STATUS, msg);
}
/* Enable/disable Download and Readme based on g_selFile */
function updateFileButtons() {
var dis = (g_selFile < 0);
gui.setDisabled(win, ID_DOWNLOAD, dis);
gui.setDisabled(win, ID_README, dis);
}
/* Disable or enable all interactive gadgets.
* NOTE: ID_SEARCH_STR (string gadget) is excluded
* because GA_Disabled on STRING_KIND can hang. */
function setAllDisabled(flag) {
gui.setDisabled(win, ID_CAT, flag);
gui.setDisabled(win, ID_SORT, flag);
gui.setDisabled(win, ID_ORDER, flag);
gui.setDisabled(win, ID_SUBCAT, flag);
gui.setDisabled(win, ID_FILES, flag);
gui.setDisabled(win, ID_DOWNLOAD, flag);
gui.setDisabled(win, ID_README, flag);
gui.setDisabled(win, ID_SEARCH, flag);
}
/* Scan html for all substrings between literal before/after markers */
function extractBetween(html, before, after) {
var results = [];
var pos = 0;
var s, e;
while (true) {
s = html.indexOf(before, pos);
if (s === -1) break;
s += before.length;
e = html.indexOf(after, s);
if (e === -1) break;
results.push(html.substring(s, e));
pos = e + after.length;
}
return results;
}
/* Strip all HTML tags from a string */
function stripTags(s) {
var out = '', inTag = false, i, c;
for (i = 0; i < s.length; i++) {
c = s.charAt(i);
if (c === '<') { inTag = true; }
else if (c === '>') { inTag = false; }
else if (!inTag) { out += c; }
}
return out;
}
/* Trim leading/trailing whitespace */
function trimStr(s) {
return s.replace(/^\s+|\s+$/g, '');
}
/* Pad/truncate string to exactly w characters for column alignment */
function col(s, w) {
s = String(s);
while (s.length < w) { s += ' '; }
return s.length > w ? s.substring(0, w) : s;
}
/* Strip tag-attribute prefix "...>" from a raw <td chunk, then stripTags */
function tdText(raw) {
var gt = raw.indexOf('>');
return trimStr(stripTags(gt >= 0 ? raw.substring(gt + 1) : raw));
}
/* ================================================================== */
/* Readme viewer window */
/* ================================================================== */
function closeReadmeWin() {
if (readmeWin) {
gui.closeWindow(readmeWin);
readmeWin = null;
}
}
/* Shift all readme lines by g_rmScroll and update listview */
function updateReadmeView() {
var shifted = [];
var i, ln;
if (!readmeWin) return;
for (i = 0; i < g_rmLines.length; i++) {
ln = g_rmLines[i];
if (g_rmScroll > 0 && ln.length > g_rmScroll) {
shifted.push(ln.substring(g_rmScroll));
} else if (g_rmScroll > 0) {
shifted.push('');
} else {
shifted.push(ln);
}
}
gui.set(readmeWin, 1, shifted);
}
function doReadme() {
var fi, dot, base, readmeName, pathBase, url;
var res, text, maxLen, maxCols, winW, i;
if (g_selFile < 0 || g_selFile >= g_files.length) {
setStatus('Select a file first');
return;
}
fi = g_files[g_selFile];
/* Replace extension with .readme */
dot = fi.name.lastIndexOf('.');
base = (dot > 0) ? fi.name.substring(0, dot) : fi.name;
readmeName = base + '.readme';
/* Build URL from fi.path */
dot = fi.path.lastIndexOf('.');
pathBase = (dot > 0) ? fi.path.substring(0, dot) : fi.path;
url = AMINET + '/' + pathBase + '.readme';
setStatus('Fetching ' + readmeName + '...');
setAllDisabled(true);
try {
res = http.get(url);
if (!res || !res.body || res.body.length === 0) {
/* Fallback: strip one more extension
* e.g. file.tar.gz -> file.tar.readme
* failed, now try file.readme */
dot = pathBase.lastIndexOf('.');
if (dot > 0) {
pathBase = pathBase.substring(0, dot);
url = AMINET + '/' +
pathBase + '.readme';
res = http.get(url);
}
}
if (!res || !res.body || res.body.length === 0) {
setStatus('No readme for ' + fi.name);
setAllDisabled(false);
updateFileButtons();
return;
}
/* Split into lines, strip CR */
text = String(res.body).replace(/\r/g, '');
g_rmLines = text.split('\n');
g_rmScroll = 0;
/* Find longest line for scroller range */
g_rmMaxLen = 0;
for (i = 0; i < g_rmLines.length; i++) {
if (g_rmLines[i].length > g_rmMaxLen)
g_rmMaxLen = g_rmLines[i].length;
}
winW = 560;
maxCols = Math.floor((winW - 30) / fw);
/* Close previous readme window */
closeReadmeWin();
/* Open readme window: listview + h-scroller */
readmeWin = gui.createWindow({
title: readmeName,
width: winW,
height: 300,
left: 60,
top: 30,
resizable: true,
minWidth: 200,
minHeight: 100,
gadgets: [
{ kind: 'listview', id: 1,
left: 0, top: 0,
width: '100%', height: -(rh + sp),
flex: true,
items: g_rmLines },
{ kind: 'scroller', id: 2,
left: 0, top: -(rh),
width: '100%', height: rh,
min: 0, max: g_rmMaxLen,
value: 0, visible: maxCols }
]
});
setStatus('Readme: ' + readmeName +
' (' + g_rmLines.length + ' lines)');
} catch (ex) {
setStatus('Readme error: ' +
(ex.message || String(ex)));
}
setAllDisabled(false);
updateFileButtons();
}
/* ================================================================== */
/* Display subcategories for a category (from hardcoded G_SUBCATS) */
/* ================================================================== */
function loadSubcats(cat) {
g_subcats = G_SUBCATS[cat] || [];
g_subcatIdx = -1;
g_files = [];
g_selFile = -1;
g_lastCode = -1;
gui.set(win, ID_FILES, ['Select a subcategory...']);
updateFileButtons();
if (g_subcats.length === 0) {
gui.set(win, ID_SUBCAT, []);
setStatus('No subcategories for ' + cat);
} else {
gui.set(win, ID_SUBCAT, g_subcats);
setStatus(cat + ': ' + g_subcats.length +
' subcategories');
}
}
/* ================================================================== */
/* Sorting and display helpers */
/* ================================================================== */
/* Parse size string like "123K", "1.2M" to bytes for comparison */
function parseSizeKB(s) {
var n, last;
if (!s || s.length === 0) return 0;
last = s.charAt(s.length - 1);
n = parseFloat(s);
if (isNaN(n)) return 0;
if (last === 'M' || last === 'm') return n * 1024;
if (last === 'G' || last === 'g') return n * 1024 * 1024;
return n; /* already K or plain number */
}
/* Sort g_files in-place according to g_sortBy / g_sortDir */
function sortFiles() {
/* Note: g_files.sort(compareFiles) produces undefined values
* due to engine bug — using manual sort as workaround */
var i, j, tmp;
for (i = 1; i < g_files.length; i++) {
tmp = g_files[i];
j = i - 1;
while (j >= 0 && compareFiles(g_files[j], tmp) > 0) {
g_files[j + 1] = g_files[j];
j--;
}
g_files[j + 1] = tmp;
}
}
function compareFiles(a, b) {
var cmp = 0;
if (g_sortBy === 0) {
/* name */
cmp = a.name < b.name ? -1 : (a.name > b.name ? 1 : 0);
} else if (g_sortBy === 1) {
/* size */
cmp = parseSizeKB(a.size) - parseSizeKB(b.size);
} else {
/* date — string comparison works for YYYY-MM-DD format */
cmp = a.date < b.date ? -1 : (a.date > b.date ? 1 : 0);
}
return g_sortDir ? -cmp : cmp;
}
/* Rebuild display list from g_files and update listview */
function refreshFileList() {
var displayList = [];
var i, f;
for (i = 0; i < g_files.length; i++) {
f = g_files[i];
displayList.push(col(f.name, 28) + ' ' +
col(f.size, 5) + ' ' + f.date);
}
g_selFile = -1;
g_lastCode = -1;
updateFileButtons();
if (displayList.length === 0) {
gui.set(win, ID_FILES, ['(empty)']);
} else {
gui.set(win, ID_FILES, displayList);
}
}
/* ================================================================== */
/* Fetch and display file listing for category/subcategory */
/* ================================================================== */
/*
* Parse file rows from an HTML page and append to g_files.
* Returns the number of new entries found.
*/
function parseRows(html, cat, sub) {
var linkPat, pos, rs, re, row, ls, le;
var name, size, date, desc;
var tds, i, t, count;
linkPat = 'href="/' + cat + '/' + sub + '/';
pos = 0;
count = 0;
while (true) {
rs = html.indexOf('<tr', pos);
if (rs === -1) break;
re = html.indexOf('</tr>', rs);
if (re === -1) break;
row = html.substring(rs, re + 5);
pos = re + 5;
/* Does this row contain a file download link? */
ls = row.indexOf(linkPat);
if (ls === -1) continue;
ls += linkPat.length;
le = row.indexOf('"', ls);
if (le === -1) continue;
name = row.substring(ls, le);
/* Reject empty names or sub-paths */
if (name.length === 0 || name.indexOf('/') !== -1) { continue; }
try {
/* Extract <td> cells */
tds = extractBetween(row, '<td', '</td>');
size = ''; date = ''; desc = '';
for (i = 0; i < tds.length; i++) {
t = tdText(tds[i]);
if (i === 4) { size = t; }
else if (i === 5) { date = t; }
else if (i === 7) { desc = t; }
}
} catch (ex2) {
/* If cell parsing fails, use defaults */
console.log('parse warning: ' + name +
': ' + String(ex2));
size = ''; date = ''; desc = '';
}
g_files.push({ name: name,
path: cat + '/' + sub + '/' + name,
size: size, date: date, desc: desc });
count++;
}
return count;
}
/*
* Detect whether aminet HTML contains a link to the next page.
* Returns the next page number, or -1 if no more pages.
*/
function findNextPage(html, currentPage) {
var needle = 'page=' + (currentPage + 1);
if (html.indexOf(needle) !== -1) {
return currentPage + 1;
}
return -1;
}
/* Map g_sortBy (0=name,1=size,2=date) to aminet sort parameter */
function aminetSortParam() {
if (g_sortBy === 1) return 'size';
if (g_sortBy === 2) return 'date';
return 'name';
}
/* Map g_sortDir (0=asc,1=desc) to aminet ord parameter */
function aminetOrdParam() {
return g_sortDir ? 'DESC' : 'ASC';
}
function loadFiles(cat, sub) {
var baseUrl, url, res, html, errMsg;
var page, nextPage, pageCount;
setStatus('Loading ' + cat + '/' + sub + '...');
g_files = [];
g_selFile = -1;
g_lastCode = -1;
gui.set(win, ID_FILES, ['Loading...']);
/* Disable all gadgets during network fetch */
setAllDisabled(true);
baseUrl = AMINET + '/' + cat + '/' + sub;
page = 1; /* aminet pages are 1-based */
try {
while (true) {
url = baseUrl + '?sort=' + aminetSortParam() +
'&ord=' + aminetOrdParam() +
'&page=' + page;
if (page > 1) {
setStatus('Loading ' + cat + '/' + sub +
' (page ' + page + ')...');
}
res = http.get(url);
if (!res || !res.body) {
if (page === 1) {
setStatus('No response from aminet.net');
gui.set(win, ID_FILES,
['(no response)']);
setAllDisabled(false);
updateFileButtons();
return;
}
break; /* no more pages */
}
html = String(res.body);
pageCount = parseRows(html, cat, sub);
/* If this page yielded no files, we're done */
if (pageCount === 0) {
break;
}
/* Check if there is a next page link */
nextPage = findNextPage(html, page);
if (nextPage === -1) {
break;
}
page = nextPage;
}
} catch (ex) {
errMsg = ex && ex.message ? ex.message : String(ex);
console.log('loadFiles fetch error: ' + errMsg);
if (g_files.length === 0) {
setStatus('Fetch error: ' + errMsg);
gui.set(win, ID_FILES,
['Fetch error: ' + errMsg]);
setAllDisabled(false);
updateFileButtons();
return;
}
/* Partial results — show what we got */
}
/* Re-enable all gadgets */
setAllDisabled(false);
updateFileButtons();
if (g_files.length === 0) {
setStatus('No files in ' + cat + '/' + sub);
gui.set(win, ID_FILES, ['(empty)']);
} else {
sortFiles();
refreshFileList();
setStatus(cat + '/' + sub + ': ' +
g_files.length + ' files' +
(page > 1 ? ' (' + page + ' pages)' : ''));
}
}
/* ================================================================== */
/* Download selected file — ASL file requester for destination */
/* ================================================================== */
function doDownload() {
var fi, url, dest, res;
if (g_selFile < 0 || g_selFile >= g_files.length) {
setStatus('Select a file from the list first');
return;
}
fi = g_files[g_selFile];
url = AMINET + '/' + fi.path;
/* Ask user where to save */
dest = gui.fileRequest({
title: 'Save: ' + fi.name,
save: true,
drawer: 'RAM:',
file: fi.name
});
if (!dest) {
setStatus('Download cancelled');
return;
}
/* Disable all gadgets during download */
setAllDisabled(true);
setStatus('Downloading ' + fi.name + ' ...');
try {
res = http.get(url);
if (!res || !res.body) {
setStatus('Download failed: empty response');
return;
}
fs.writeFileSync(dest.path, res.body);
setStatus('Saved ' + fi.name + ' (' +
res.body.length + ' B) \xbb ' +
dest.path);
} catch (ex) {
setStatus('Download error: ' +
(ex.message || String(ex)));
} finally {
/* Re-enable all gadgets */
setAllDisabled(false);
updateFileButtons();
}
}
/* ================================================================== */
/* Search aminet.net */
/* ================================================================== */
/*
* Parse search result rows from aminet search HTML.
* Search results have links like href="/cat/sub/filename".
* Returns count of entries appended to g_files.
*/
function parseSearchRows(html) {
var pos, rs, re, row, ls, le, href;
var name, size, date, desc, path;
var tds, i, t, count, parts;
pos = 0;
count = 0;
while (true) {
rs = html.indexOf('<tr', pos);
if (rs === -1) break;
re = html.indexOf('</tr>', rs);
if (re === -1) break;
row = html.substring(rs, re + 5);
pos = re + 5;
/* Look for file download links like href="/cat/sub/file" */
ls = row.indexOf('href="/');
if (ls === -1) continue;
ls += 7; /* skip href="/ */
le = row.indexOf('"', ls);
if (le === -1) continue;
href = row.substring(ls, le);
/* Must have exactly 2 slashes: cat/sub/file */
parts = href.split('/');
if (parts.length !== 3) continue;
if (parts[2].length === 0) continue;
/* Skip non-file links (dirs, pages, etc.) */
if (parts[2].indexOf('.') === -1) continue;
name = parts[2];
path = href;
try {
tds = extractBetween(row, '<td', '</td>');
size = ''; date = ''; desc = '';
for (i = 0; i < tds.length; i++) {
t = tdText(tds[i]);
if (i === 4) { size = t; }
else if (i === 5) { date = t; }
else if (i === 7) { desc = t; }
}
} catch (ex2) {
size = ''; date = ''; desc = '';
}
g_files.push({ name: name, path: path,
size: size, date: date, desc: desc });
count++;
}
return count;
}
function doSearch() {
var query, url, res, html, count;
/* Read search term from string gadget */
query = trimStr(gui.get(win, ID_SEARCH_STR) || '');
if (query.length === 0) {
setStatus('Type a search term in Find');
return;
}
setStatus('Searching ALL of Aminet: ' +
query + '...');
g_files = [];
g_selFile = -1;
g_lastCode = -1;
gui.set(win, ID_FILES, ['Searching...']);
setAllDisabled(true);
try {
url = AMINET + '/search?query=' + query;
res = http.get(url);
if (!res || !res.body) {
setStatus('No response from aminet.net');
gui.set(win, ID_FILES, ['(no response)']);
setAllDisabled(false);
updateFileButtons();
return;
}
html = String(res.body);
count = parseSearchRows(html);
} catch (ex) {
setStatus('Search error: ' +
(ex.message || String(ex)));
gui.set(win, ID_FILES,
['Search error']);
setAllDisabled(false);
updateFileButtons();
return;
}
setAllDisabled(false);
updateFileButtons();
if (g_files.length === 0) {
setStatus('No results for: ' + query);
gui.set(win, ID_FILES, ['(no results)']);
} else {
sortFiles();
refreshFileList();
setStatus('Found ' + g_files.length +
' in all Aminet for: ' + query);
}
}
/* ================================================================== */
/* Startup — display subcategories for first category */
/* ================================================================== */
loadSubcats(G_CATS[g_catIdx]);
/* ================================================================== */
/* Main event loop (polls both windows via waitTOF) */
/* ================================================================== */
/* Process a single main-window event; returns false to quit */
function handleEvent(evt) {
if (evt.type === 'close') return false;
if (evt.type === 'key' && evt.key === '\x1b') return false;
if (evt.type === 'gadgetup') {
if (evt.id === ID_DOWNLOAD) {
doDownload();
} else if (evt.id === ID_README) {
doReadme();
} else if (evt.id === ID_SEARCH) {
doSearch();
} else if (evt.id === ID_SEARCH_STR) {
/* Enter pressed in search field */
doSearch();
} else if (evt.id === ID_CAT) {
g_catIdx = evt.code;
loadSubcats(G_CATS[g_catIdx]);
} else if (evt.id === ID_SORT) {
g_sortBy = evt.code;
if (g_files.length > 0) {
setAllDisabled(true);
sortFiles();
refreshFileList();
setAllDisabled(false);
updateFileButtons();
}
} else if (evt.id === ID_ORDER) {
g_sortDir = evt.code;
if (g_files.length > 0) {
setAllDisabled(true);
sortFiles();
refreshFileList();
setAllDisabled(false);
updateFileButtons();
}
} else if (evt.id === ID_SUBCAT) {
g_subcatIdx = evt.code;
g_lastCode = -1;
if (g_subcatIdx >= 0 &&
g_subcatIdx < g_subcats.length) {
loadFiles(G_CATS[g_catIdx],
g_subcats[g_subcatIdx]);
}
} else if (evt.id === ID_FILES) {
if (evt.code === g_lastCode) {
g_selFile = evt.code;
g_lastCode = -1;
doDownload();
} else {
g_selFile = evt.code;
g_lastCode = evt.code;
updateFileButtons();
if (g_selFile >= 0 &&
g_selFile < g_files.length) {
var finfo = g_files[g_selFile];
var info = finfo.name;
if (finfo.size) {
info += ' ' + finfo.size;
}
if (finfo.desc) {
info += ' ' + finfo.desc;
}
setStatus(info);
}
}
}
}
return true;
}
var running = true;
while (running) {
var evt, revt;
if (readmeWin) {
/* Both windows open — poll both, sleep on TOF */
evt = gui.pollEvent(win);
revt = gui.pollEvent(readmeWin);
if (revt) {
if (revt.type === 'close') {
closeReadmeWin();
} else if (revt.type === 'resize') {
/* Recalculate visible chars for new width */
var newCols = Math.floor(
(revt.width - 30) / fw);
gui.set(readmeWin, 2,
{ visible: newCols,
total: g_rmMaxLen });
} else if (revt.id === 2) {
g_rmScroll = gui.get(readmeWin, 2);
updateReadmeView();
}
}
if (!evt) {
gfx.waitTOF();
continue;
}
} else {
/* Only main window — block efficiently */
evt = gui.waitEvent(win);
if (!evt) break;
}
if (!handleEvent(evt)) {
running = false;
}
}
closeReadmeWin();
gui.closeWindow(win);