Initial commit: NAUT torrent web UI with stubbed server

Advanced torrent client web UI aimed at power users, with a
zero-dependency Node stub server (built-in http + SSE) serving live
mock data.

- Dense sortable/multi-select torrent grid with live updates
- Detail panel: General/Trackers/Peers/Content/Pieces (resizable)
- Sidebar filters: status, categories, tags, trackers
- Create/delete categories and tags (sidebar + right-click)
- Add via magnet or client-side-parsed .torrent upload
- RSS, integrated search, and read-only engine views
- Hand-drawn SVG icon set, dark theme, keyboard shortcuts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-16 21:15:31 -04:00
commit bc1be49a37
12 changed files with 2677 additions and 0 deletions

87
public/js/format.js Normal file
View file

@ -0,0 +1,87 @@
// Display formatters — units, durations, dates, states.
export function bytes(n, dp = 1) {
if (n == null || isNaN(n)) return '';
if (n === 0) return '0 B';
const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
const i = Math.min(u.length - 1, Math.floor(Math.log(n) / Math.log(1024)));
return `${(n / 1024 ** i).toFixed(i === 0 ? 0 : dp)} ${u[i]}`;
}
export function rate(n) {
if (!n) return '';
return `${bytes(n)}/s`;
}
export function eta(sec) {
if (sec == null || sec >= 8640000 || sec < 0) return '∞';
if (sec < 1) return '0s';
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = Math.floor(sec % 60);
if (d) return `${d}d ${h}h`;
if (h) return `${h}h ${m}m`;
if (m) return `${m}m ${s}s`;
return `${s}s`;
}
export function duration(sec) {
if (sec == null || sec < 0) return '';
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
if (d) return `${d}d ${h}h ${m}m`;
if (h) return `${h}h ${m}m`;
return `${m}m`;
}
export function pct(p) { return `${(p * 100).toFixed(1)}%`; }
export function ratio(r) {
if (r == null) return '';
if (r >= 9999) return '∞';
return r.toFixed(2);
}
export function date(ms) {
if (!ms || ms < 0) return '';
const d = new Date(ms);
return d.toLocaleString(undefined, { year: '2-digit', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
export function ago(ms) {
if (!ms || ms < 0) return '';
const s = (Date.now() - ms) / 1000;
if (s < 60) return 'just now';
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
return `${Math.floor(s / 86400)}d ago`;
}
const STATE_LABEL = {
downloading: 'Downloading', forcedDL: '[F] Downloading', metaDL: 'Fetching metadata',
stalledDL: 'Stalled (DL)', uploading: 'Seeding', forcedUP: '[F] Seeding',
stalledUP: 'Seeding (idle)', pausedDL: 'Paused', pausedUP: 'Completed',
checkingDL: 'Checking', checkingUP: 'Checking', moving: 'Moving',
queuedDL: 'Queued', queuedUP: 'Queued', error: 'Error', missingFiles: 'Missing files',
};
export function stateLabel(s) { return STATE_LABEL[s] || s; }
const STATE_COLOR = {
downloading: 'var(--dl)', forcedDL: 'var(--dl)', metaDL: 'var(--dl)',
uploading: 'var(--up)', forcedUP: 'var(--up)',
stalledDL: 'var(--txt-dim)', stalledUP: 'var(--txt-dim)',
pausedDL: 'var(--pause)', pausedUP: 'var(--pause)',
checkingDL: 'var(--check)', checkingUP: 'var(--check)', moving: 'var(--check)',
queuedDL: 'var(--queue)', queuedUP: 'var(--queue)',
error: 'var(--err)', missingFiles: 'var(--err)',
};
export function stateColor(s) { return STATE_COLOR[s] || 'var(--txt-dim)'; }
const PRIO_LABEL = { 0: 'Do not download', 1: 'Normal', 6: 'High', 7: 'Maximum' };
export function filePrio(p) { return PRIO_LABEL[p] ?? 'Normal'; }
export function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}