Merge pull request #2517 from wipedlifepotato/JS_CLIENT_TORRENT

peers + files (torrent js client)
This commit is contained in:
orignal
2026-08-24 16:31:21 -04:00
committed by GitHub
2 changed files with 277 additions and 47 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ class TorrentClient {
method: "POST",
body: JSON.stringify({
'method': 'torrent-get',
'arguments': { "fields": ["id", "name", "status", "rateDownload", "rateUpload", "totalSize", "percentDone"] }
'arguments': { "fields": ["id", "name", "status", "rateDownload", "rateUpload", "totalSize", "percentDone", "files", "peers"] }
}),
headers: { 'Content-Type': 'application/json' }
});
+276 -46
View File
@@ -1,49 +1,83 @@
<style>
:root {
--bg-color: #0f172a;
--card-bg: #1e293b;
--border-color: #334155;
--primary: #3b82f6;
--primary-hover: #2563eb;
--danger: #ef4444;
--danger-hover: #dc2626;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--radius: 6px;
--bg-color: #050b05;
--card-bg: #0a140a;
--border-color: #1b4d1b;
--primary: #00ff66;
--primary-hover: #00cc52;
--danger: #ff3333;
--danger-hover: #cc0000;
--text-main: #00ff66;
--text-muted: #33994d;
--radius: 4px;
--main-text-color: #359dff;
}
/* Эффект старого монитора со сканирующими линиями */
.torrent-wrapper {
background-color: var(--bg-color);
color: var(--text-main);
padding: 20px;
width: 100%;
width: 100%;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.torrent-wrapper::before {
content: " ";
display: block;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
z-index: 10;
background-size: 100% 3px, 3px 100%;
pointer-events: none;
}
.torrent-wrapper .container {
width: 100%;
max-width: none;
max-width: none;
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
z-index: 11;
}
.torrent-wrapper * {
box-sizing: border-box;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
text-shadow: 0 0 5px rgba(0, 255, 102, 0.4);
}
.torrent-wrapper header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
border-bottom: 1px dashed var(--border-color);
padding-bottom: 15px;
}
.header-title-block {
display: flex;
align-items: center;
gap: 15px;
}
/* Иконка пришельца в Base64 (мини SVG) */
.alien-icon {
width: 36px;
height: 36px;
filter: drop-shadow(0 0 4px var(--primary));
}
.torrent-wrapper header h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.025em;
color: var(--primary);
}
.torrent-wrapper .action-bar {
@@ -53,11 +87,12 @@
padding: 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.15s ease;
transition: all 0.2s ease;
}
.torrent-wrapper .action-bar:hover, .torrent-wrapper .action-bar.dragover {
border-color: var(--primary);
box-shadow: inset 0 0 10px rgba(0, 255, 102, 0.1);
}
.torrent-wrapper .table-container {
@@ -80,7 +115,7 @@
}
.torrent-wrapper th {
background: rgba(0, 0, 0, 0.2);
background: rgba(0, 255, 102, 0.05);
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
@@ -91,6 +126,14 @@
border-bottom: none;
}
.torrent-wrapper tr.torrent-row {
cursor: pointer;
transition: background 0.15s ease;
}
.torrent-wrapper tr.torrent-row:hover {
background: rgba(0, 255, 102, 0.08);
}
.torrent-wrapper .progress-wrapper {
display: flex;
align-items: center;
@@ -99,10 +142,10 @@
.torrent-wrapper .progress-bar {
flex-grow: 1;
background: var(--bg-color);
border-radius: 4px;
background: #020802;
border-radius: 2px;
height: 6px;
overflow: scroll;
overflow: hidden;
border: 1px solid var(--border-color);
}
@@ -110,6 +153,7 @@
background: var(--primary);
height: 100%;
width: 0%;
box-shadow: 0 0 8px var(--primary);
}
.torrent-wrapper .empty-state {
@@ -119,41 +163,122 @@
}
.torrent-wrapper .btn {
background-color: var(--primary);
color: white;
border: none;
background-color: transparent;
color: var(--primary);
border: 1px solid var(--primary);
padding: 6px 12px;
font-size: 0.75rem;
font-weight: 500;
border-radius: var(--radius);
cursor: pointer;
transition: all 0.2s;
}
.torrent-wrapper .btn:hover {
background-color: var(--primary);
color: #000;
box-shadow: 0 0 8px var(--primary);
}
.torrent-wrapper .btn-secondary {
border-color: var(--text-muted);
color: var(--text-muted);
margin-right: 5px;
}
.torrent-wrapper .btn-secondary:hover {
background-color: var(--text-muted);
color: #000;
box-shadow: 0 0 8px var(--text-muted);
}
.torrent-wrapper .btn-danger {
background-color: transparent;
border-color: var(--danger);
color: var(--danger);
border: 1px solid var(--danger);
}
.torrent-wrapper .btn-danger:hover {
background-color: var(--danger);
color: white;
color: #fff;
box-shadow: 0 0 8px var(--danger);
}
/* Модальное окно */
.torrent-modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 5, 0, 0.85);
justify-content: center;
align-items: center;
z-index: 1000;
}
.torrent-modal.active {
display: flex;
}
.modal-content {
background: var(--card-bg);
border: 1px solid var(--primary);
border-radius: var(--radius);
width: 800px;
max-width: 90%;
max-height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 255, 102, 0.2);
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0, 255, 102, 0.03);
}
.modal-body {
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.modal-section h3 {
margin: 0 0 10px 0;
font-size: 0.9rem;
color: var(--text-muted);
text-transform: uppercase;
}
.content {
max-width: 100em; /*fix for torrent interface*/
max-width: 100em;
}
/* Мигающий курсор терминала */
.cursor-blink::after {
content: "_";
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
</style>
<div class="torrent-wrapper">
<div class="container">
<header>
<h1>TORRENT_DAEMON_INTERFACE</h1>
<div class="header-title-block">
<!-- Чистый Inline SVG пришельца с неоновым свечением -->
<svg class="alien-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--primary)">
<path d="M12 2C6.48 2 2 6.48 2 12c0 4.84 3.44 8.88 8 9.82v-2.18C6.98 17.06 5 14.23 5 12c0-3.87 3.13-7 7-7s7 3.13 7 7c0 2.23-1.98 5.06-5 5.82v2.18c4.56-.28 8-4.3 8-8.82C22 6.48 17.52 2 12 2zm0 6c-1.65 0-3 1.35-3 3s1.35 3 3 3 3-1.35 3-3-1.35-3-3-3zm-4 6c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm8 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm-4 3c-2.33 0-4 2.67-4 4h8c0-1.33-1.67-4-4-4z"/>
</svg>
<h1>TORRENT_DAEMON_INTERFACE<span class="cursor-blink"></span></h1>
</div>
<div id="connection_status" style="font-size: 0.75rem; color: var(--text-muted);">CONNECTING...</div>
</header>
<div id="drop_zone" class="action-bar">
<span>Drop torrent payload here or <span style="color: var(--primary); text-decoration: underline;">browse file</span></span>
<span>[+] DROP_TORRENT_PAYLOAD_OR_<span style="color: var(--primary); text-decoration: underline;">BROWSE_FILE</span></span>
<input type="file" id="torrentf" accept=".torrent" style="display:none"/>
</div>
@@ -172,7 +297,7 @@
</thead>
<tbody id="torrents_table_body">
<tr>
<td colspan="6" class="empty-state">No active transfers</td>
<td colspan="7" class="empty-state">NO_ACTIVE_TRANSFERS_DETECTED</td>
</tr>
</tbody>
</table>
@@ -180,7 +305,54 @@
</div>
</div>
<!-- Модальное окно для файлов и пиров -->
<div id="torrent_details_modal" class="torrent-modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal_torrent_title" style="margin: 0; font-size: 1rem; color: var(--primary);">Details</h2>
<button class="btn btn-secondary" onclick="closeDetailsModal()">[X] CLOSE</button>
</div>
<div class="modal-body">
<div class="modal-section">
<h3>Files Manifest</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>File Name</th>
<th>Size</th>
<th>Completed</th>
<th>Progress</th>
</tr>
</thead>
<tbody id="modal_files_body"></tbody>
</table>
</div>
</div>
<div class="modal-section">
<h3>Active Nodes / Peers (<span id="peer_count">0</span>)</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Address</th>
<th>Client</th>
<th>Port</th>
<th>Downloading</th>
<th>Speed</th>
</tr>
</thead>
<tbody id="modal_peers_body"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
let currentTorrentsData = [];
(function() {
setInterval(async () => {
const statusIndicator = document.getElementById('connection_status');
@@ -191,32 +363,31 @@
try {
const r = await window.tjs.getTorrents();
if (!r || !r.result || !r.result.torrents) {
statusIndicator.textContent = 'STATUS: INVALID RESPONSE';
statusIndicator.textContent = 'STATUS: INVALID_RESPONSE';
return;
}
statusIndicator.textContent = 'STATUS: CONNECTED';
statusIndicator.style.color = '#22c55e';
statusIndicator.textContent = 'STATUS: SECURE_CONNECTED';
statusIndicator.style.color = '#00ff66';
const tbody = document.getElementById('torrents_table_body');
const torrents = r.result.torrents;
currentTorrentsData = r.result.torrents;
if (torrents.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No active transfers</td></tr>';
if (currentTorrentsData.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">NO_ACTIVE_TRANSFERS_DETECTED</td></tr>';
return;
}
tbody.innerHTML = "";
torrents.forEach((el) => {
const percent = Math.round(el.percentDone);
const speed = window.tjs.formatBytes(el.rateDownload);
const speed_upload = window.tjs.formatBytes(el.rateUpload);
console.log(`speed ${el.rateDownload}/${el.rateUpload}`);
console.log(el)
const statusText = window.tjs.getStatusString(el.status);
currentTorrentsData.forEach((el) => {
const percent = el.percentDone.toFixed(2)
const speed = window.tjs.formatBytes ? window.tjs.formatBytes(el.rateDownload) : (el.rateDownload + ' B/s');
const speed_upload = window.tjs.formatBytes ? window.tjs.formatBytes(el.rateUpload) : (el.rateUpload + ' B/s');
const statusText = window.tjs.getStatusString ? window.tjs.getStatusString(el.status) : el.status;
const name = el.name || 'UNNAMED_METADATA';
const tr = document.createElement('tr');
tr.className = 'torrent-row';
tr.innerHTML = `
<td>#${el.id}</td>
<td style="max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${name}">${name}</td>
@@ -231,16 +402,17 @@
</td>
<td>${speed}</td>
<td>${speed_upload}</td>
<td style="text-align: right;">
<button class="btn btn-danger" onclick="removeTorrent(${el.id})">TERMINATE</button>
<td style="text-align: right;" onclick="event.stopPropagation()">
<button class="btn btn-secondary" onclick="openTorrentDetails(${el.id})">INFO</button>
<button class="btn btn-danger" onclick="removeTorrent(${el.id})">KILL</button>
</td>
`;
tr.onclick = () => openTorrentDetails(el.id);
tbody.appendChild(tr);
});
} catch (err) {
statusIndicator.textContent = 'STATUS: CONNECTION_REFUSED';
statusIndicator.style.color = '#ef4444';
console.log(err)
statusIndicator.style.color = '#ff3333';
}
}, 1000);
@@ -287,4 +459,62 @@
if (window.tjs) await window.tjs.removeTorrent(id);
}
})();
window.openTorrentDetails = function(id) {
const torrent = currentTorrentsData.find(t => t.id === id);
if (!torrent) return;
document.getElementById('modal_torrent_title').textContent = `DETAILS // ${torrent.name} (#${torrent.id})`;
const filesBody = document.getElementById('modal_files_body');
if (torrent.files && torrent.files.length > 0) {
filesBody.innerHTML = torrent.files.map(file => {
const fPercent = file.length > 0 ? ((file.bytesCompleted / file.length) * 100).toFixed(1) : 0;
const formatSize = (bytes) => (bytes / (1024 * 1024)).toFixed(2) + ' MB';
return `
<tr>
<td style="word-break: break-all;">${file.name}</td>
<td>${formatSize(file.length)}</td>
<td>${formatSize(file.bytesCompleted)}</td>
<td>${fPercent}%</td>
</tr>
`;
}).join('');
} else {
filesBody.innerHTML = '<tr><td colspan="4" class="empty-state">NO_FILES_DATA</td></tr>';
}
const peersBody = document.getElementById('modal_peers_body');
document.getElementById('peer_count').textContent = torrent.peers ? torrent.peers.length : 0;
if (torrent.peers && torrent.peers.length > 0) {
peersBody.innerHTML = torrent.peers.map(peer => {
const peerSpeed = window.tjs.formatBytes ? window.tjs.formatBytes(peer.rateToClient) : (peer.rateToClient + ' B/s');
return `
<tr>
<td>${peer.address}</td>
<td>${peer.clientName || 'Unknown'}</td>
<td>${peer.port}</td>
<td>${peer.isDowloadingFrom ? 'YES' : 'NO'}</td>
<td>${peerSpeed}</td>
</tr>
`;
}).join('');
} else {
peersBody.innerHTML = '<tr><td colspan="5" class="empty-state">NO_ACTIVE_PEERS</td></tr>';
}
document.getElementById('torrent_details_modal').classList.add('active');
}
window.closeDetailsModal = function() {
document.getElementById('torrent_details_modal').classList.remove('active');
}
window.onclick = function(event) {
const modal = document.getElementById('torrent_details_modal');
if (event.target === modal) {
closeDetailsModal();
}
}
</script>