page title updates and adding built in webtorrent browser client (not working yet)

This commit is contained in:
2025-01-26 20:45:45 -05:00
parent 6d781b327b
commit 229fe19d45
8 changed files with 946 additions and 4 deletions

View File

@@ -6,7 +6,7 @@
<title>bitvid | About</title>
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="BitVid - Markdown Viewer" />
<meta property="og:title" content="bitvid | About" />
<meta
property="og:description"
content="View and render markdown content dynamically."

96
src/css/torrent-style.css Normal file
View File

@@ -0,0 +1,96 @@
/* <ai_context>
File: css/torrent-style.css
Purpose: Additional styles specifically for the Torrent UI queue and table.
</ai_context> */
.torrent-queue-container {
background-color: #ffffff;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.torrent-queue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.torrent-queue-header h2 {
font-size: 1.125rem;
font-weight: 600;
}
.torrent-queue-table {
width: 100%;
border-collapse: collapse;
}
.torrent-queue-table th,
.torrent-queue-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.torrent-queue-table th {
background-color: #f9fafb;
color: #374151;
font-weight: 600;
font-size: 0.875rem;
}
.torrent-queue-table td {
font-size: 0.875rem;
color: #4b5563;
}
.torrent-progress-bar {
height: 6px;
width: 100%;
background-color: #d1d5db;
border-radius: 3px;
position: relative;
overflow: hidden;
}
.torrent-progress-fill {
height: 6px;
background-color: #10b981; /* green-500 */
transition: width 0.3s ease;
border-radius: 3px;
}
.torrent-actions button {
margin-right: 0.5rem;
outline: none;
border: none;
cursor: pointer;
color: #fff;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 3px;
}
.torrent-actions .pause-resume-btn {
background-color: #3b82f6; /* blue-500 */
}
.torrent-actions .remove-btn {
background-color: #ef4444; /* red-500 */
}
.torrent-actions .share-btn {
background-color: #6b7280; /* gray-500 */
}
.priority-select {
margin-left: 0.5rem;
font-size: 0.75rem;
padding: 0.2rem;
border: 1px solid #d1d5db;
border-radius: 3px;
background-color: #ffffff;
}

View File

@@ -6,7 +6,7 @@
<title>bitvid | Getting Started</title>
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="BitVid - Markdown Viewer" />
<meta property="og:title" content="bitvid | Getting Started" />
<meta
property="og:description"
content="View and render markdown content dynamically."

View File

@@ -420,6 +420,14 @@
>
Roadmap
</a>
<a
href="torrent.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Torrent
</a>
</div>
<p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"

View File

@@ -6,7 +6,7 @@
<title>bitvid | About</title>
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="BitVid - Markdown Viewer" />
<meta property="og:title" content="bitvid | IPNS" />
<meta
property="og:description"
content="View and render markdown content dynamically."

531
src/js/torrent-app.js Normal file
View File

@@ -0,0 +1,531 @@
/* <ai_context>
File: js/torrent-app.js
Purpose: Recreate core βTorrent-like functionality in a modern design
</ai_context> */
// We'll rely on window.WebTorrent from webtorrent.global.min.js
class TorrentApp {
constructor() {
this.torrentClient = null;
this.fileInput = null;
this.magnetInput = null;
this.seedingArea = null;
// An in-memory array of torrents
this.torrents = [];
this.selectedTorrent = null;
// UI elements
this.torrentQueue = null;
this.torrentQueueBody = null;
// Selected panel elements
this.selectedTorrentPanel = null;
this.selectedTorrentName = null;
this.pauseResumeBtn = null;
this.removeBtn = null;
this.shareList = null;
this.selectedTorrentFilesBody = null;
// Client stats elements
this.clientStatsBar = null;
this.clientDlSpeed = null;
this.clientUlSpeed = null;
this.clientRatio = null;
this.statsInterval = null;
}
async init() {
this.fileInput = document.getElementById("torrentFile");
this.magnetInput = document.getElementById("magnetInput");
this.seedingArea = document.getElementById("seedingArea");
this.torrentQueue = document.getElementById("torrentQueue");
this.torrentQueueBody = document.getElementById("torrentQueueTable").querySelector("tbody");
// Selected panel
this.selectedTorrentPanel = document.getElementById("selectedTorrentPanel");
this.selectedTorrentName = document.getElementById("selectedTorrentName");
this.pauseResumeBtn = document.getElementById("pauseResumeBtn");
this.removeBtn = document.getElementById("removeBtn");
this.shareList = document.getElementById("shareList");
this.selectedTorrentFilesBody = document.getElementById("selectedTorrentFilesBody");
// Client stats
this.clientStatsBar = document.getElementById("clientStatsBar");
this.clientDlSpeed = document.getElementById("clientDlSpeed");
this.clientUlSpeed = document.getElementById("clientUlSpeed");
this.clientRatio = document.getElementById("clientRatio");
if (this.fileInput) {
this.fileInput.addEventListener("change", (e) => this.handleFile(e));
}
const downloadBtn = document.getElementById("downloadBtn");
if (downloadBtn) {
downloadBtn.addEventListener("click", () => this.handleMagnet());
}
const seedBtn = document.getElementById("seedBtn");
if (seedBtn) {
seedBtn.addEventListener("click", () => this.handleSeeding());
}
if (this.pauseResumeBtn) {
this.pauseResumeBtn.addEventListener("click", () => {
if (!this.selectedTorrent) return;
this.togglePause(this.selectedTorrent);
this.renderSelectedTorrent();
});
}
if (this.removeBtn) {
this.removeBtn.addEventListener("click", () => {
if (!this.selectedTorrent) return;
this.removeTorrent(this.selectedTorrent.infoHash);
this.clearSelectedTorrent();
});
}
// Create local WebTorrent client from global
if (window.WebTorrent) {
this.torrentClient = new window.WebTorrent();
console.log("TorrentApp initialized with WebTorrent global.");
// Start updating client stats
this.clientStatsBar.classList.remove("hidden");
this.statsInterval = setInterval(() => {
if (!this.torrentClient) return;
this.clientDlSpeed.textContent = "↓ " + this.formatBytes(this.torrentClient.downloadSpeed || 0) + "/s";
this.clientUlSpeed.textContent = "↑ " + this.formatBytes(this.torrentClient.uploadSpeed || 0) + "/s";
// ratio is not directly exposed, so we do a basic placeholder
const ratio = ((this.torrentClient.uploaded || 0) / ((this.torrentClient.downloaded || 1))) || 0;
this.clientRatio.textContent = ratio.toFixed(2);
}, 1000);
} else {
console.error("window.WebTorrent is not defined. Please include webtorrent.global.min.js");
return;
}
}
handleFile(e) {
const file = e.target.files[0];
if (!file) return;
this.addTorrentFile(file);
}
handleMagnet() {
if (!this.torrentClient) return;
const magnetLink = this.magnetInput.value.trim();
if (!magnetLink) return;
this.addMagnet(magnetLink);
}
handleSeeding() {
if (!this.torrentClient) return;
const files = this.seedingArea.files;
if (!files || files.length === 0) return;
this.seedFiles(files);
}
addTorrentFile(file) {
console.log("Adding torrent file:", file.name);
this.torrentClient.add(file, (torrent) => {
this.trackTorrent(torrent);
});
}
addMagnet(magnetURI) {
console.log("Adding magnet:", magnetURI);
this.torrentClient.add(magnetURI, (torrent) => {
this.trackTorrent(torrent);
});
}
seedFiles(fileList) {
console.log("Seeding", fileList.length, "files");
this.torrentClient.seed(fileList, (torrent) => {
this.trackTorrent(torrent, true);
});
}
trackTorrent(torrent, isSeeding = false) {
// If we already have it in the table, skip
const existing = this.torrents.find((t) => t.infoHash === torrent.infoHash);
if (existing) {
console.log("Torrent already tracked:", torrent.infoHash);
return;
}
this.torrents.push(torrent);
// Show queue if hidden
if (this.torrentQueue && this.torrentQueue.classList.contains("hidden")) {
this.torrentQueue.classList.remove("hidden");
}
// Create a row in the queue
this.createTorrentRow(torrent, isSeeding);
torrent.on("done", () => {
console.log(torrent.name, "finished downloading.");
});
// Generate Blob URLs for each file
if (torrent.files && torrent.files.forEach) {
torrent.files.forEach((file) => {
file.getBlobURL((err) => {
if (err) {
console.error("File blob error:", err);
}
});
});
}
// Periodic UI updates
this.updateTorrentUI(torrent);
}
createTorrentRow(torrent, isSeeding) {
if (!this.torrentQueueBody) return;
const row = document.createElement("tr");
row.id = `torrent-row-${torrent.infoHash}`;
// Name cell
const nameCell = document.createElement("td");
nameCell.textContent = torrent.name || "Unnamed Torrent";
// Progress cell
const progressCell = document.createElement("td");
progressCell.style.width = "150px"; // for visual space
const progressBar = document.createElement("div");
progressBar.classList.add("torrent-progress-bar");
const progressFill = document.createElement("div");
progressFill.classList.add("torrent-progress-fill");
progressFill.style.width = "0%";
progressBar.appendChild(progressFill);
progressCell.appendChild(progressBar);
// Size cell
const sizeCell = document.createElement("td");
sizeCell.textContent = this.formatBytes(torrent.length || 0);
// DL Speed
const dlSpeedCell = document.createElement("td");
dlSpeedCell.textContent = "0 KB/s";
// UL Speed
const ulSpeedCell = document.createElement("td");
ulSpeedCell.textContent = "0 KB/s";
// Peers
const peersCell = document.createElement("td");
peersCell.textContent = "0";
// ETA
const etaCell = document.createElement("td");
etaCell.textContent = "∞";
// Actions
const actionsCell = document.createElement("td");
actionsCell.classList.add("torrent-actions");
const pauseResumeBtn = document.createElement("button");
pauseResumeBtn.classList.add("pause-resume-btn");
pauseResumeBtn.textContent = "Pause";
pauseResumeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.togglePause(torrent);
if (this.selectedTorrent === torrent) {
this.renderSelectedTorrent();
}
});
const removeBtn = document.createElement("button");
removeBtn.classList.add("remove-btn");
removeBtn.textContent = "Remove";
removeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.removeTorrent(torrent.infoHash);
if (this.selectedTorrent === torrent) {
this.clearSelectedTorrent();
}
});
const shareBtn = document.createElement("button");
shareBtn.classList.add("share-btn");
shareBtn.textContent = "Share";
shareBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.shareMagnetLink(torrent);
});
actionsCell.appendChild(pauseResumeBtn);
actionsCell.appendChild(removeBtn);
actionsCell.appendChild(shareBtn);
row.appendChild(nameCell);
row.appendChild(progressCell);
row.appendChild(sizeCell);
row.appendChild(dlSpeedCell);
row.appendChild(ulSpeedCell);
row.appendChild(peersCell);
row.appendChild(etaCell);
row.appendChild(actionsCell);
// Clicking the entire row => selectTorrent
row.addEventListener("click", () => {
this.selectTorrent(torrent);
});
this.torrentQueueBody.appendChild(row);
}
selectTorrent(torrent) {
this.selectedTorrent = torrent;
this.renderSelectedTorrent();
}
clearSelectedTorrent() {
this.selectedTorrent = null;
if (this.selectedTorrentPanel) {
this.selectedTorrentPanel.classList.add("hidden");
}
}
renderSelectedTorrent() {
if (!this.selectedTorrentPanel) return;
if (!this.selectedTorrent) {
this.selectedTorrentPanel.classList.add("hidden");
return;
}
const t = this.selectedTorrent;
// Show panel
this.selectedTorrentPanel.classList.remove("hidden");
// Name
this.selectedTorrentName.textContent = t.name || "Unnamed Torrent";
// Pause/Resume
if (t.paused) {
this.pauseResumeBtn.textContent = "Resume";
} else {
this.pauseResumeBtn.textContent = "Pause";
}
// Share links
while (this.shareList.firstChild) {
this.shareList.removeChild(this.shareList.firstChild);
}
// Example links
if (t.magnetURI) {
const magnetLi = document.createElement("li");
const magnetLink = document.createElement("a");
magnetLink.href = t.magnetURI;
magnetLink.target = "_blank";
magnetLink.textContent = "Magnet URI";
magnetLi.appendChild(magnetLink);
this.shareList.appendChild(magnetLi);
}
if (t.infoHash) {
const hashLi = document.createElement("li");
hashLi.innerHTML = `<strong>Hash: </strong>${t.infoHash}`;
this.shareList.appendChild(hashLi);
}
// We won't do the .torrent file link unless you want to generate it
// Files
while (this.selectedTorrentFilesBody.firstChild) {
this.selectedTorrentFilesBody.removeChild(this.selectedTorrentFilesBody.firstChild);
}
if (t.files) {
t.files.forEach((file) => {
const tr = document.createElement("tr");
// File name
const nameTd = document.createElement("td");
if (file.done) {
const a = document.createElement("a");
a.href = file.url || "#";
a.download = file.name;
a.target = "_self";
a.textContent = file.name;
nameTd.appendChild(a);
} else {
nameTd.textContent = file.name;
}
// Size
const sizeTd = document.createElement("td");
sizeTd.textContent = this.formatBytes(file.length);
// Priority
const priorityTd = document.createElement("td");
const select = document.createElement("select");
select.classList.add("no-margin", "border", "rounded", "text-sm");
const optHigh = document.createElement("option");
optHigh.value = "1";
optHigh.textContent = "High Priority";
const optLow = document.createElement("option");
optLow.value = "0";
optLow.textContent = "Low Priority";
const optNone = document.createElement("option");
optNone.value = "-1";
optNone.textContent = "Don't download";
select.appendChild(optHigh);
select.appendChild(optLow);
select.appendChild(optNone);
// default
select.value = file.priority || "0";
select.addEventListener("change", () => {
this.changeFilePriority(file, select.value);
});
priorityTd.appendChild(select);
tr.appendChild(nameTd);
tr.appendChild(sizeTd);
tr.appendChild(priorityTd);
this.selectedTorrentFilesBody.appendChild(tr);
});
}
}
changeFilePriority(file, val) {
file.priority = val;
if (val === "-1") {
file.deselect && file.deselect();
} else {
// In real webtorrent usage:
// file.select(Number(val));
file.select && file.select(Number(val));
}
}
togglePause(torrent) {
if (!torrent.paused) {
torrent.pause && torrent.pause();
torrent.paused = true;
} else {
torrent.resume && torrent.resume();
torrent.paused = false;
}
}
removeTorrent(infoHash) {
this.torrents = this.torrents.filter((t) => t.infoHash !== infoHash);
const row = document.getElementById(`torrent-row-${infoHash}`);
if (row && row.parentNode) {
row.parentNode.removeChild(row);
}
if (!this.torrentClient) return;
const torrent = this.torrentClient.get(infoHash);
if (torrent && torrent.destroy) {
torrent.destroy(() => {
console.log(`Destroyed torrent ${torrent.name}`);
});
}
if (this.selectedTorrent && this.selectedTorrent.infoHash === infoHash) {
this.clearSelectedTorrent();
}
if (this.torrents.length === 0 && this.torrentQueue) {
this.torrentQueue.classList.add("hidden");
}
}
shareMagnetLink(torrent) {
const link = torrent.magnetURI || "No magnet available";
navigator.clipboard
.writeText(link)
.then(() => {
console.log("Magnet link copied to clipboard!");
})
.catch((err) => {
console.error("Failed to copy magnet link:", err);
});
}
// Periodically update the queue
updateTorrentUI(torrent) {
const row = document.getElementById(`torrent-row-${torrent.infoHash}`);
if (!row) return;
const progressCell = row.children[1];
const progressFill = progressCell.querySelector(".torrent-progress-fill");
const dlSpeedCell = row.children[3];
const ulSpeedCell = row.children[4];
const peersCell = row.children[5];
const etaCell = row.children[6];
const refresh = () => {
if (!document.body.contains(row)) return;
// If using real webtorrent, you'd do:
// let progress = torrent.progress
// let downloadSpeed = torrent.downloadSpeed
// let uploadSpeed = torrent.uploadSpeed
// let numPeers = torrent.numPeers
// etc.
const progressPercent = (torrent.progress || 0) * 100;
progressFill.style.width = progressPercent.toFixed(1) + "%";
dlSpeedCell.textContent = this.formatBytes(torrent.downloadSpeed || 0) + "/s";
ulSpeedCell.textContent = this.formatBytes(torrent.uploadSpeed || 0) + "/s";
peersCell.textContent = torrent.numPeers ? torrent.numPeers.toString() : "0";
if (torrent.done) {
etaCell.textContent = "Done";
} else {
etaCell.textContent = (torrent.downloadSpeed && torrent.downloadSpeed > 0)
? this.calcETA(torrent)
: "∞";
}
requestAnimationFrame(refresh);
};
refresh();
}
calcETA(torrent) {
// Real approach: (torrent.length - torrent.downloaded) / torrent.downloadSpeed
const bytesRemaining = (torrent.length || 0) - (torrent.downloaded || 0);
if (!torrent.downloadSpeed || torrent.downloadSpeed <= 0) return "∞";
const sec = bytesRemaining / torrent.downloadSpeed;
if (sec < 1) return "<1s";
let s = Math.floor(sec);
const h = Math.floor(s / 3600);
s = s % 3600;
const m = Math.floor(s / 60);
s = s % 60;
const parts = [];
if (h > 0) parts.push(h + "h");
if (m > 0) parts.push(m + "m");
parts.push(s + "s");
return parts.join(" ");
}
formatBytes(num) {
if (num <= 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(num) / Math.log(k));
return (num / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
}
}
// Initialize on DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
const app = new TorrentApp();
app.init();
});

View File

@@ -6,7 +6,7 @@
<title>bitvid | Roadmap</title>
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="BitVid - Markdown Viewer" />
<meta property="og:title" content="bitvid | Roadmap" />
<meta
property="og:description"
content="View and render markdown content dynamically."

307
src/torrent.html Normal file
View File

@@ -0,0 +1,307 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>bitvid | Local Torrent Client</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Open Graph Meta Tags (optional) -->
<meta property="og:title" content="bitvid | Local Torrent Client" />
<meta
property="og:description"
content="Download and Seed Torrents Seamlessly"
/>
<meta
property="og:image"
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
/>
<meta property="og:url" content="https://bitvid.network" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<!-- App Icons -->
<link rel="icon" href="assets/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="assets/png/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="assets/png/favicon-16x16.png"
/>
<link rel="manifest" href="site.webmanifest" />
<meta name="theme-color" content="#0f172a" />
<!-- Styles (Tailwind + your own CSS) -->
<link
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/torrent-style.css" />
</head>
<body class="bg-gray-100">
<!-- Main container to mirror the style of index.html -->
<div
id="app"
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
>
<!-- Header -->
<header class="mb-8">
<div class="flex items-start">
<!-- Logo links back to index.html (or "/") -->
<a href="index.html">
<img
src="assets/svg/bitvid-logo-light-mode.svg"
alt="BitVid Logo"
class="h-16"
/>
</a>
</div>
</header>
<!-- Torrent Download Section -->
<div class="bg-white p-6 rounded-lg shadow-md mb-8">
<h2 class="text-xl font-semibold mb-4">Download a Torrent</h2>
<div class="flex items-center space-x-3 mb-4">
<input
type="text"
id="magnetInput"
placeholder="Enter magnet URI"
class="w-full border border-gray-300 rounded px-2 py-2 focus:outline-none focus:ring focus:ring-blue-500"
/>
<button
id="downloadBtn"
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Download
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2"
>Or select a torrent file</label
>
<input
type="file"
id="torrentFile"
accept=".torrent"
class="border border-gray-300 rounded px-2 py-1 focus:outline-none"
/>
</div>
</div>
<!-- Torrent Seeding Section -->
<div class="bg-white p-6 rounded-lg shadow-md mb-8">
<h2 class="text-xl font-semibold mb-4">Seed Files</h2>
<input
type="file"
id="seedingArea"
multiple
class="border border-gray-300 rounded px-2 py-1 focus:outline-none mb-4"
/>
<button
id="seedBtn"
class="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500"
>
Start Seeding
</button>
</div>
<!-- Torrent Queue Section -->
<div
id="torrentQueue"
class="torrent-queue-container hidden bg-white rounded-lg shadow-md p-6 flex-grow flex flex-col"
>
<div class="torrent-queue-header flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Active Torrents</h2>
</div>
<div class="overflow-auto grow">
<table class="torrent-queue-table w-full" id="torrentQueueTable">
<thead>
<tr>
<th>Name</th>
<th>Progress</th>
<th>Size</th>
<th>↓ Speed</th>
<th>↑ Speed</th>
<th>Peers</th>
<th>ETA</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="torrentQueueBody"></tbody>
</table>
</div>
</div>
<!-- Selected Torrent Details Panel -->
<div
id="selectedTorrentPanel"
class="bg-white p-6 rounded-lg shadow-md mt-8 hidden"
>
<!-- Name & Controls -->
<h5
id="selectedTorrentName"
class="text-lg font-bold mb-2 flex items-center space-x-2"
>
<!-- Title goes here -->
</h5>
<div class="flex items-center space-x-4 mb-4">
<button
id="pauseResumeBtn"
class="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600 focus:outline-none"
>
Pause
</button>
<button
id="removeBtn"
class="bg-red-500 text-white px-4 py-1 rounded hover:bg-red-600 focus:outline-none"
>
Remove
</button>
</div>
<!-- Share Section -->
<h6 class="font-semibold mb-1">Share</h6>
<ul class="list-disc list-inside text-sm mb-4" id="shareList"></ul>
<!-- Files List -->
<h5 class="text-md font-semibold mb-2">Files</h5>
<table class="min-w-full text-sm" id="selectedTorrentFilesTable">
<thead class="bg-gray-100">
<tr>
<th class="px-3 py-2 text-gray-700">Name</th>
<th class="px-3 py-2 text-gray-700">Size</th>
<th class="px-3 py-2 text-gray-700">Priority</th>
</tr>
</thead>
<tbody id="selectedTorrentFilesBody"></tbody>
</table>
</div>
<!-- Client Stats Bar -->
<div
id="clientStatsBar"
class="text-center text-sm text-gray-600 mt-8 hidden"
>
<strong>
Client Stats:
<span id="clientDlSpeed">↓ 0 kB/s</span> ·
<span id="clientUlSpeed">↑ 0 kB/s</span> ·
Ratio: <span id="clientRatio">0.00</span>
</strong>
</div>
<!-- Footer -->
<footer class="mt-auto pb-8 text-center px-4">
<a
href="http://bitvid.network/"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.network
</a>
|
<a
href="https://bitvid.btc.us"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.btc.us
</a>
|
<a
href="https://bitvid.eth.limo"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
bitvid.eth.limo
</a>
|
<div class="mt-2 space-x-4">
<a
href="https://github.com/PR0M3TH3AN/bitvid"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
<a
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
Nostr
</a>
<a
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_blank"
rel="noopener noreferrer"
>
Blog
</a>
<a
href="getting-started.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Getting Started
</a>
<a
href="about.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
About
</a>
<a
href="roadmap.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Roadmap
</a>
<a
href="torrent.html"
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
target="_self"
rel="noopener noreferrer"
>
Torrent
</a>
</div>
<p
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
>
IPNS:
<a href="ipns.html" class="text-blue-600 underline">
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
</a>
</p>
</footer>
</div>
<!-- Reference files that actually exist -->
<!-- 1) Service Worker in the root -->
<script src="sw.min.js"></script>
<!-- 2) Local webtorrent.min.js (not “global”) -->
<script src="js/webtorrent.min.js"></script>
<!-- 3) Our main torrent app logic -->
<script type="module" src="js/torrent-app.js"></script>
</body>
</html>