Files
ares-next/server/static/index.html

307 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ares Next — Tracker P2P</title>
<style>
:root {
--bg: #0e1117; --panel: #161b24; --border: #262d3a;
--text: #e8ecf3; --muted: #8b95a7; --accent: #4f8cff;
--accent2: #4ade80; --danger: #f87171;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
header { border-bottom: 1px solid var(--border); background: var(--panel); position: sticky; top: 0; z-index: 10; }
.head-inner { max-width: 1000px; margin: 0 auto; padding: 14px 20px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 18px; }
.logo .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--accent2); box-shadow: 0 0 12px var(--accent2); }
.stats { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
.stat { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 4px 10px; font-size: 12px; color: var(--muted); }
.stat b { color: var(--text); font-size: 14px; }
main { max-width: 1000px; margin: 0 auto; padding: 24px 20px 80px; }
.searchbar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
input[type=text], select { background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 10px 14px; font-size: 14px; outline: none; }
input[type=text]:focus, select:focus { border-color: var(--accent); }
.searchbar input[type=text] { flex: 1; min-width: 220px; }
button { background: var(--accent); color: white; border: none; border-radius: 8px; padding: 10px 18px; font-size: 14px; font-weight: 600; cursor: pointer; }
button:hover { filter: brightness(1.1); }
button.ghost { background: transparent; border: 1px solid var(--border); color: var(--muted); }
button.ghost:hover { color: var(--text); border-color: var(--muted); }
.filters { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.filters select { flex: 1; min-width: 140px; }
.count { color: var(--muted); font-size: 13px; margin: 4px 0 12px; }
.list { display: flex; flex-direction: column; gap: 10px; }
.file { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; cursor: pointer; transition: border-color .15s, transform .05s; }
.file:hover { border-color: var(--accent); }
.file .top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.file .name { font-weight: 600; font-size: 15px; word-break: break-all; }
.badge { font-size: 11px; padding: 2px 8px; border-radius: 99px; background: #22304a; color: var(--accent); text-transform: uppercase; letter-spacing: .03em; }
.badge.size { background: #123c2a; color: var(--accent2); }
.file .desc { color: var(--muted); font-size: 13px; margin-top: 6px; }
.file .meta { color: var(--muted); font-size: 12px; margin-top: 6px; font-family: ui-monospace, monospace; word-break: break-all; }
.empty { text-align: center; color: var(--muted); padding: 60px 0; }
.empty b { display: block; font-size: 18px; margin-bottom: 6px; color: var(--text); }
.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 20px; font-size: 14px; display: none; z-index: 100; max-width: 90vw; }
.toast.show { display: block; }
.toast.ok { border-color: var(--accent2); }
.toast.err { border-color: var(--danger); }
dialog { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; color: var(--text); padding: 24px; max-width: 560px; width: 90%; }
dialog::backdrop { background: rgba(0,0,0,.6); }
dialog h2 { margin: 0 0 16px; font-size: 18px; word-break: break-word; }
dialog .row { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
dialog .row span { color: var(--muted); flex-shrink: 0; }
dialog .row code { font-family: ui-monospace, monospace; word-break: break-all; text-align: right; }
dialog .actions { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
.peer { display: inline-block; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 2px 8px; font-size: 12px; font-family: ui-monospace, monospace; margin: 2px 4px 2px 0; }
.peers { margin-top: 14px; }
.peers .title { font-size: 13px; color: var(--muted); margin-bottom: 6px; }
dialog .actions .copy { background: transparent; border: 1px solid var(--accent); color: var(--accent); }
dialog .actions .copy:hover { color: white; background: var(--accent); }
details { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; margin-top: 24px; padding: 14px 16px; }
details summary { cursor: pointer; font-weight: 600; font-size: 14px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
.form-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
.form-grid label.full { grid-column: 1 / -1; }
.form-grid input, .form-grid select { background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: 8px 10px; font-size: 13px; outline: none; font-family: inherit; }
.form-grid input:focus { border-color: var(--accent); }
.form-actions { margin-top: 14px; display: flex; justify-content: flex-end; }
</style>
</head>
<body>
<header>
<div class="head-inner">
<div class="logo"><span class="dot"></span> Ares Next</div>
<div class="stats">
<div class="stat">Arquivos <b id="st-files">–</b></div>
<div class="stat">Peers online <b id="st-peers">–</b></div>
<div class="stat">Total <b id="st-bytes">–</b></div>
<div class="stat">v<span id="st-ver">–</span></div>
</div>
</div>
</header>
<main>
<div class="searchbar">
<input type="text" id="q" placeholder="Buscar por nome, descrição, hash..." autofocus>
<button id="btn-search">Buscar</button>
<button class="ghost" id="btn-clear">Limpar</button>
</div>
<div class="filters">
<select id="ftype">
<option value="">Todos os tipos</option>
<option value="audio">Áudio</option>
<option value="video">Vídeo</option>
<option value="document">Documento</option>
<option value="software">Software</option>
<option value="other">Outro</option>
</select>
<select id="sort">
<option value="relevance">Mais relevantes</option>
<option value="newest">Mais recentes</option>
<option value="oldest">Mais antigos</option>
<option value="size_desc">Maior tamanho</option>
<option value="size_asc">Menor tamanho</option>
</select>
<select id="limit">
<option value="10">10 resultados</option>
<option value="25" selected>25 resultados</option>
<option value="50">50 resultados</option>
<option value="100">100 resultados</option>
</select>
</div>
<div class="count" id="count"></div>
<div class="list" id="list">
<div class="empty"><b>Ainda não há arquivos</b>Busque acima ou anuncie um arquivo para começar.</div>
</div>
<details>
<summary>Anunciar arquivo (POST /api/v1/announce)</summary>
<div class="form-grid">
<label class="full">Nome *<input id="f-name" type="text" placeholder="ubuntu-24.04.iso"></label>
<label class="full">Descrição<input id="f-desc" type="text" placeholder="O que é este arquivo?"></label>
<label>Tipo
<select id="f-type">
<option value="software">Software</option>
<option value="audio">Áudio</option>
<option value="video">Vídeo</option>
<option value="document">Documento</option>
<option value="other">Outro</option>
</select>
</label>
<label>Extensão<input id="f-ext" type="text" placeholder="iso"></label>
<label>Peer ID *<input id="f-peer" type="text" placeholder="meu-peer-id"></label>
<label>Tamanho (bytes) *<input id="f-size" type="number" min="0" placeholder="734003200"></label>
<label class="full">Hash SHA-256 / BLAKE3 (64 hex) *<input id="f-hash" type="text" maxlength="64" placeholder="0123...abc"></label>
<label class="full">Licença<input id="f-license" type="text" placeholder="MIT"></label>
</div>
<div class="form-actions"><button id="btn-announce">Anunciar arquivo</button></div>
</details>
</main>
<div class="toast" id="toast"></div>
<dialog id="dlg">
<h2 id="dlg-name"></h2>
<div id="dlg-body"></div>
<div class="actions">
<button id="dlg-close">Fechar</button>
</div>
</dialog>
<script>
const $ = id => document.getElementById(id);
const fmtSize = b => {
if (!b) return "0 B";
const u = ["B","KB","MB","GB","TB"]; let i = 0;
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
return b.toFixed(i ? 1 : 0) + " " + u[i];
};
const fmtDate = ts => new Date(ts * 1000).toLocaleString("pt-BR");
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
let toastTimer;
function toast(msg, ok) {
const t = $("toast");
t.textContent = msg;
t.className = "toast show " + (ok ? "ok" : "err");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.className = "toast", 4000);
}
const api = async (path, opts) => {
const r = await fetch(path, opts);
if (!r.ok) {
let msg = "HTTP " + r.status;
try { const j = await r.json(); if (j.error) msg = j.error; else if (j.message) msg = j.message; } catch {}
throw new Error(msg);
}
return r.json();
};
async function loadStats() {
try {
const s = await api("/api/v1/stats");
$("st-files").textContent = s.files;
$("st-peers").textContent = s.online_peers;
$("st-bytes").textContent = fmtSize(s.total_bytes);
$("st-ver").textContent = s.version;
} catch {}
}
async function search() {
const q = $("q").value.trim();
const params = new URLSearchParams();
if (q) params.set("q", q);
if ($("ftype").value) params.set("ftype", $("ftype").value);
if ($("sort").value) params.set("sort", $("sort").value);
params.set("limit", $("limit").value);
const list = $("list");
list.innerHTML = '<div class="empty"><b>Buscando...</b></div>';
try {
const rows = await api("/api/v1/search?" + params.toString());
$("count").textContent = rows.length ? rows.length + " resultado(s)" : "";
if (!rows.length) {
list.innerHTML = '<div class="empty"><b>Nenhum resultado</b>Tente outros termos de busca.</div>';
return;
}
list.innerHTML = rows.map(f => `
<div class="file" data-id="${esc(f.id)}">
<div class="top">
<span class="name">${esc(f.name)}</span>
<span class="badge">${esc(f.ftype)}</span>
${f.extension ? '<span class="badge size">.' + esc(f.extension) + "</span>" : ""}
<span class="badge size">${fmtSize(f.size_bytes)}</span>
</div>
${f.description ? `<div class="desc">${esc(f.description)}</div>` : ""}
<div class="meta">${esc(f.id)} · ${fmtDate(f.created_at)}</div>
</div>`).join("");
document.querySelectorAll(".file").forEach(el => {
el.addEventListener("click", () => openFile(el.dataset.id));
});
} catch (e) {
list.innerHTML = '<div class="empty"><b>Erro na busca</b>' + esc(e.message) + "</div>";
}
}
async function openFile(id) {
const dlg = $("dlg");
$("dlg-name").textContent = id;
$("dlg-body").innerHTML = '<div class="empty">Carregando...</div>';
dlg.showModal();
try {
const f = await api("/api/v1/files/" + encodeURIComponent(id));
$("dlg-name").textContent = f.name;
let peers = [];
try { peers = (await api("/api/v1/peers/" + encodeURIComponent(id))).peers; } catch {}
const row = (k, v) => `<div class="row"><span>${k}</span><code>${v}</code></div>`;
$("dlg-body").innerHTML =
(f.description ? `<div style="font-size:13px;color:var(--muted);margin-bottom:10px">${esc(f.description)}</div>` : "") +
row("Peer", esc(f.peer_id)) +
row("Tipo", esc(f.ftype) + (f.extension ? " · ." + esc(f.extension) : "")) +
row("Tamanho", fmtSize(f.size_bytes) + " (" + f.size_bytes + " bytes)") +
row("Licença", esc(f.license || "—")) +
row("Tags", esc((f.tags || []).join(", ") || "—")) +
row("Criado em", fmtDate(f.created_at)) +
row("Hash", esc(f.hash)) +
`<div class="peers"><div class="title">Peers online (${peers.length})</div>` +
(peers.length ? peers.map(p => `<span class="peer">${esc(p)}</span>`).join("") : "Nenhum peer online") +
"</div>";
const actions = $("dlg").querySelector(".actions");
const oldCopy = actions.querySelectorAll(".copy");
oldCopy.forEach(b => b.remove());
const copy = document.createElement("button");
copy.className = "copy";
copy.textContent = "Copiar hash";
copy.addEventListener("click", () => {
navigator.clipboard.writeText(f.hash).then(() => toast("Hash copiado", true)).catch(() => toast("Falha ao copiar", false));
});
actions.insertBefore(copy, $("dlg-close"));
} catch (e) {
$("dlg-body").innerHTML = '<div class="empty">' + esc(e.message) + "</div>";
}
}
async function announce() {
const body = {
peer_id: $("f-peer").value.trim(),
name: $("f-name").value.trim(),
description: $("f-desc").value.trim(),
type: $("f-type").value,
extension: $("f-ext").value.trim(),
size_bytes: Number($("f-size").value),
hash: $("f-hash").value.trim(),
license: $("f-license").value.trim(),
tags: []
};
if (!body.peer_id || !body.name || !body.size_bytes || !body.hash) {
toast("Preencha peer_id, nome, tamanho e hash", false);
return;
}
const btn = $("btn-announce");
btn.disabled = true;
try {
const r = await api("/api/v1/announce", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
toast("Anunciado: " + r.file_id, true);
loadStats();
search();
$("f-name").value = $("f-desc").value = $("f-peer").value = $("f-size").value = $("f-hash").value = $("f-license").value = "";
} catch (e) {
toast(e.message, false);
} finally {
btn.disabled = false;
}
}
$("btn-search").addEventListener("click", search);
$("q").addEventListener("keydown", e => { if (e.key === "Enter") search(); });
$("btn-clear").addEventListener("click", () => { $("q").value = ""; $("ftype").value = ""; $("sort").value = "relevance"; search(); });
["ftype", "sort", "limit"].forEach(id => $(id).addEventListener("change", search));
$("btn-announce").addEventListener("click", announce);
$("dlg-close").addEventListener("click", () => $("dlg").close());
loadStats();
search();
</script>
</body>
</html>