mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
added links to βTorrent in footer
This commit is contained in:
@@ -157,6 +157,14 @@
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://btorrent.xyz/"
|
||||
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"
|
||||
|
@@ -1,96 +0,0 @@
|
||||
/* <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;
|
||||
}
|
@@ -157,6 +157,14 @@
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://btorrent.xyz/"
|
||||
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"
|
||||
|
@@ -421,12 +421,12 @@
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="torrent.html"
|
||||
href="https://btorrent.xyz/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Torrent
|
||||
βTorrent
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
|
@@ -156,6 +156,14 @@
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://btorrent.xyz/"
|
||||
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"
|
||||
|
@@ -1,531 +0,0 @@
|
||||
/* <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();
|
||||
});
|
@@ -138,6 +138,14 @@
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://btorrent.xyz/"
|
||||
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"
|
||||
|
307
src/torrent.html
307
src/torrent.html
@@ -1,307 +0,0 @@
|
||||
<!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>
|
403
src/torrent/app.js
Normal file
403
src/torrent/app.js
Normal file
@@ -0,0 +1,403 @@
|
||||
/* global WebTorrent, angular, moment, prompt */
|
||||
|
||||
const VERSION = '1.1'
|
||||
const trackers = ['wss://tracker.btorrent.xyz', 'wss://tracker.openwebtorrent.com']
|
||||
const rtcConfig = {
|
||||
'iceServers': [
|
||||
{
|
||||
'urls': ['stun:stun.l.google.com:19305', 'stun:stun1.l.google.com:19305']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const torrentOpts = {
|
||||
announce: trackers
|
||||
}
|
||||
|
||||
const trackerOpts = {
|
||||
announce: trackers,
|
||||
rtcConfig: rtcConfig
|
||||
}
|
||||
|
||||
const debug = window.localStorage.getItem('debug') !== null
|
||||
|
||||
function dbg (message, item, color = '#333333') {
|
||||
if (debug) {
|
||||
if (item && item.name) {
|
||||
console.debug(
|
||||
`%cβTorrent:${item.infoHash ? 'torrent ' : 'torrent ' + item._torrent.name + ':file '}${item.name}${item.infoHash ? ' (' + item.infoHash + ')' : ''} %c${message}`,
|
||||
'color: #33C3F0',
|
||||
`color: ${color}`
|
||||
)
|
||||
} else {
|
||||
console.debug(`%cβTorrent:client %c${message}`, 'color: #33C3F0', `color: ${color}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function er (err, item) {
|
||||
dbg(err, item, '#FF0000')
|
||||
}
|
||||
|
||||
dbg(`Starting v${VERSION}. WebTorrent ${WebTorrent.VERSION}`)
|
||||
|
||||
// Create WebTorrent client
|
||||
const client = new WebTorrent({ tracker: trackerOpts })
|
||||
|
||||
// Angular app
|
||||
const app = angular.module('BTorrent', [
|
||||
'ngRoute',
|
||||
'ui.grid',
|
||||
'ui.grid.resizeColumns',
|
||||
'ui.grid.selection',
|
||||
'ngFileUpload',
|
||||
'ngNotify'
|
||||
], [
|
||||
'$compileProvider',
|
||||
'$locationProvider',
|
||||
'$routeProvider',
|
||||
function ($compileProvider, $locationProvider, $routeProvider) {
|
||||
// Allow magnet: and blob: links
|
||||
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|magnet|blob|javascript):/)
|
||||
|
||||
// Disable HTML5 mode, only use # routing so no rewrites are needed
|
||||
$locationProvider.html5Mode(false).hashPrefix('')
|
||||
|
||||
// Define routes
|
||||
$routeProvider
|
||||
.when('/view', {
|
||||
templateUrl: 'views/view.html',
|
||||
controller: 'ViewCtrl'
|
||||
})
|
||||
.when('/download', {
|
||||
templateUrl: 'views/download.html',
|
||||
controller: 'DownloadCtrl'
|
||||
})
|
||||
.otherwise({
|
||||
templateUrl: 'views/full.html',
|
||||
controller: 'FullCtrl'
|
||||
})
|
||||
}
|
||||
])
|
||||
|
||||
app.controller('BTorrentCtrl', [
|
||||
'$scope',
|
||||
'$rootScope',
|
||||
'$http',
|
||||
'$log',
|
||||
'$location',
|
||||
'ngNotify',
|
||||
function ($scope, $rootScope, $http, $log, $location, ngNotify) {
|
||||
$rootScope.version = VERSION
|
||||
$rootScope.webtorrentVersion = WebTorrent.VERSION
|
||||
|
||||
ngNotify.config({
|
||||
duration: 5000,
|
||||
html: true
|
||||
})
|
||||
|
||||
if (!WebTorrent.WEBRTC_SUPPORT) {
|
||||
$rootScope.disabled = true
|
||||
ngNotify.set('Please use a WebRTC compatible browser', {
|
||||
type: 'error',
|
||||
sticky: true,
|
||||
button: false
|
||||
})
|
||||
}
|
||||
|
||||
$rootScope.client = client
|
||||
|
||||
function updateAll () {
|
||||
if (!$rootScope.client.processing) {
|
||||
$rootScope.$applyAsync()
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(updateAll, 500)
|
||||
|
||||
$rootScope.seedFiles = function (files) {
|
||||
if (files && files.length > 0) {
|
||||
dbg(`Seeding ${files.length} file(s)`)
|
||||
$rootScope.client.processing = true
|
||||
$rootScope.client.seed(files, torrentOpts, $rootScope.onSeed)
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.openTorrentFile = function (file) {
|
||||
if (file) {
|
||||
dbg(`Adding torrent file ${file.name}`)
|
||||
$rootScope.client.processing = true
|
||||
$rootScope.client.add(file, torrentOpts, $rootScope.onTorrent)
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.client.on('error', function (err, torrent) {
|
||||
$rootScope.client.processing = false
|
||||
ngNotify.set(err, 'error')
|
||||
er(err, torrent)
|
||||
})
|
||||
|
||||
$rootScope.addMagnet = function (magnet, onTorrent) {
|
||||
if (magnet && magnet.length > 0) {
|
||||
dbg(`Adding magnet/hash ${magnet}`)
|
||||
$rootScope.client.processing = true
|
||||
$rootScope.client.add(magnet, torrentOpts, onTorrent || $rootScope.onTorrent)
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.destroyedTorrent = function (err) {
|
||||
if (err) throw err
|
||||
dbg('Destroyed torrent', $rootScope.selectedTorrent)
|
||||
$rootScope.selectedTorrent = null
|
||||
$rootScope.client.processing = false
|
||||
}
|
||||
|
||||
$rootScope.changePriority = function (file) {
|
||||
if (file.priority === '-1') {
|
||||
dbg('Deselected', file)
|
||||
file.deselect()
|
||||
} else {
|
||||
dbg(`Selected with priority ${file.priority}`, file)
|
||||
file.select(file.priority)
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.onTorrent = function (torrent, isSeed) {
|
||||
dbg(torrent.magnetURI)
|
||||
torrent.safeTorrentFileURL = torrent.torrentFileBlobURL
|
||||
torrent.fileName = `${torrent.name}.torrent`
|
||||
if (!isSeed) {
|
||||
dbg('Received metadata', torrent)
|
||||
ngNotify.set(`Received ${torrent.name} metadata`)
|
||||
if (!$rootScope.selectedTorrent) {
|
||||
$rootScope.selectedTorrent = torrent
|
||||
}
|
||||
$rootScope.client.processing = false
|
||||
}
|
||||
torrent.files.forEach(function (file) {
|
||||
file.getBlobURL(function (err, url) {
|
||||
if (err) throw err
|
||||
file.url = url
|
||||
if (isSeed) {
|
||||
dbg('Started seeding', torrent)
|
||||
if (!$rootScope.selectedTorrent) {
|
||||
$rootScope.selectedTorrent = torrent
|
||||
}
|
||||
$rootScope.client.processing = false
|
||||
} else {
|
||||
dbg('Done ', file)
|
||||
ngNotify.set(`<b>${file.name}</b> ready for download`, 'success')
|
||||
}
|
||||
})
|
||||
})
|
||||
torrent.on('done', function () {
|
||||
if (!isSeed) {
|
||||
dbg('Done', torrent)
|
||||
ngNotify.set(`<b>${torrent.name}</b> has finished downloading`, 'success')
|
||||
}
|
||||
})
|
||||
torrent.on('wire', function (wire, addr) {
|
||||
dbg(`Wire ${addr}`, torrent)
|
||||
})
|
||||
torrent.on('error', er)
|
||||
}
|
||||
|
||||
$rootScope.onSeed = function (torrent) {
|
||||
$rootScope.onTorrent(torrent, true)
|
||||
}
|
||||
|
||||
dbg('Angular ready')
|
||||
}
|
||||
])
|
||||
|
||||
// Full View Controller
|
||||
app.controller('FullCtrl', [
|
||||
'$scope',
|
||||
'$rootScope',
|
||||
'$http',
|
||||
'$log',
|
||||
'$location',
|
||||
'ngNotify',
|
||||
function ($scope, $rootScope, $http, $log, $location, ngNotify) {
|
||||
ngNotify.config({
|
||||
duration: 5000,
|
||||
html: true
|
||||
})
|
||||
$scope.addMagnet = function () {
|
||||
$rootScope.addMagnet($scope.torrentInput)
|
||||
$scope.torrentInput = ''
|
||||
}
|
||||
|
||||
$scope.columns = [
|
||||
{ field: 'name', cellTooltip: true, minWidth: 200 },
|
||||
{ field: 'length', name: 'Size', cellFilter: 'pbytes', width: 80 },
|
||||
{ field: 'received', displayName: 'Downloaded', cellFilter: 'pbytes', width: 135 },
|
||||
{ field: 'downloadSpeed', displayName: '↓ Speed', cellFilter: 'pbytes:1', width: 100 },
|
||||
{ field: 'progress', displayName: 'Progress', cellFilter: 'progress', width: 100 },
|
||||
{ field: 'timeRemaining', displayName: 'ETA', cellFilter: 'humanTime', width: 140 },
|
||||
{ field: 'uploaded', displayName: 'Uploaded', cellFilter: 'pbytes', width: 125 },
|
||||
{ field: 'uploadSpeed', displayName: '↑ Speed', cellFilter: 'pbytes:1', width: 100 },
|
||||
{ field: 'numPeers', displayName: 'Peers', width: 80 },
|
||||
{ field: 'ratio', cellFilter: 'number:2', width: 80 }
|
||||
]
|
||||
|
||||
$scope.gridOptions = {
|
||||
columnDefs: $scope.columns,
|
||||
data: $rootScope.client.torrents,
|
||||
enableColumnResizing: true,
|
||||
enableColumnMenus: false,
|
||||
enableRowSelection: true,
|
||||
enableRowHeaderSelection: false,
|
||||
multiSelect: false
|
||||
}
|
||||
|
||||
$scope.gridOptions.onRegisterApi = function (gridApi) {
|
||||
$scope.gridApi = gridApi
|
||||
gridApi.selection.on.rowSelectionChanged($scope, function (row) {
|
||||
if (!row.isSelected && $rootScope.selectedTorrent && $rootScope.selectedTorrent.infoHash === row.entity.infoHash) {
|
||||
$rootScope.selectedTorrent = null
|
||||
} else {
|
||||
$rootScope.selectedTorrent = row.entity
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If there's a magnet in the URL (ex: torrent.html#/magnet-link)
|
||||
if ($location.hash() !== '') {
|
||||
$rootScope.client.processing = true
|
||||
setTimeout(function () {
|
||||
dbg(`Adding ${$location.hash()}`)
|
||||
$rootScope.addMagnet($location.hash())
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Download View Controller
|
||||
app.controller('DownloadCtrl', [
|
||||
'$scope',
|
||||
'$rootScope',
|
||||
'$http',
|
||||
'$log',
|
||||
'$location',
|
||||
'ngNotify',
|
||||
function ($scope, $rootScope, $http, $log, $location, ngNotify) {
|
||||
ngNotify.config({
|
||||
duration: 5000,
|
||||
html: true
|
||||
})
|
||||
|
||||
$scope.addMagnet = function () {
|
||||
$rootScope.addMagnet($scope.torrentInput)
|
||||
$scope.torrentInput = ''
|
||||
}
|
||||
|
||||
if ($location.hash() !== '') {
|
||||
$rootScope.client.processing = true
|
||||
setTimeout(function () {
|
||||
dbg(`Adding ${$location.hash()}`)
|
||||
$rootScope.addMagnet($location.hash())
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Stream/View Controller
|
||||
app.controller('ViewCtrl', [
|
||||
'$scope',
|
||||
'$rootScope',
|
||||
'$http',
|
||||
'$log',
|
||||
'$location',
|
||||
'ngNotify',
|
||||
function ($scope, $rootScope, $http, $log, $location, ngNotify) {
|
||||
ngNotify.config({
|
||||
duration: 2000,
|
||||
html: true
|
||||
})
|
||||
|
||||
function onTorrent (torrent) {
|
||||
// Adjust viewer styling
|
||||
$rootScope.viewerStyle = {
|
||||
'margin-top': '-20px',
|
||||
'text-align': 'center'
|
||||
}
|
||||
dbg(torrent.magnetURI)
|
||||
torrent.safeTorrentFileURL = torrent.torrentFileBlobURL
|
||||
torrent.fileName = `${torrent.name}.torrent`
|
||||
$rootScope.selectedTorrent = torrent
|
||||
$rootScope.client.processing = false
|
||||
dbg('Received metadata', torrent)
|
||||
ngNotify.set(`Received ${torrent.name} metadata`)
|
||||
|
||||
// Append each file to #viewer
|
||||
torrent.files.forEach(function (file) {
|
||||
file.appendTo('#viewer')
|
||||
file.getBlobURL(function (err, url) {
|
||||
if (err) throw err
|
||||
file.url = url
|
||||
dbg('Done ', file)
|
||||
})
|
||||
})
|
||||
|
||||
torrent.on('done', function () {
|
||||
dbg('Done', torrent)
|
||||
})
|
||||
torrent.on('wire', function (wire, addr) {
|
||||
dbg(`Wire ${addr}`, torrent)
|
||||
})
|
||||
torrent.on('error', er)
|
||||
}
|
||||
|
||||
$scope.addMagnet = function () {
|
||||
$rootScope.addMagnet($scope.torrentInput, onTorrent)
|
||||
$scope.torrentInput = ''
|
||||
}
|
||||
|
||||
// If there's a magnet in the URL
|
||||
if ($location.hash() !== '') {
|
||||
$rootScope.client.processing = true
|
||||
setTimeout(function () {
|
||||
dbg(`Adding ${$location.hash()}`)
|
||||
$rootScope.addMagnet($location.hash(), onTorrent)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Custom Angular filters
|
||||
app.filter('html', [
|
||||
'$sce',
|
||||
function ($sce) {
|
||||
return function (input) {
|
||||
return $sce.trustAsHtml(input)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
app.filter('pbytes', function () {
|
||||
return function (num, speed) {
|
||||
if (isNaN(num)) return ''
|
||||
if (num < 1) return speed ? '' : '0 B'
|
||||
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB']
|
||||
const exponent = Math.min(Math.floor(Math.log(num) / 6.907755278982137), 8)
|
||||
const val = (num / Math.pow(1000, exponent)).toFixed(1) * 1
|
||||
const unit = units[exponent]
|
||||
return `${val} ${unit}${speed ? '/s' : ''}`
|
||||
}
|
||||
})
|
||||
|
||||
app.filter('humanTime', function () {
|
||||
return function (millis) {
|
||||
if (millis < 1000) return ''
|
||||
const remaining = moment.duration(millis).humanize()
|
||||
return remaining.charAt(0).toUpperCase() + remaining.slice(1)
|
||||
}
|
||||
})
|
||||
|
||||
app.filter('progress', function () {
|
||||
return function (num) {
|
||||
return `${(100 * num).toFixed(1)}%`
|
||||
}
|
||||
})
|
BIN
src/torrent/favicon.ico
Normal file
BIN
src/torrent/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
202
src/torrent/style.css
Normal file
202
src/torrent/style.css
Normal file
@@ -0,0 +1,202 @@
|
||||
:root {
|
||||
--color-bg: #ffffff;
|
||||
--color-card: #1e293b;
|
||||
--color-primary: #8b5cf6;
|
||||
--color-secondary: #f43f5e;
|
||||
--color-text: #7b7b7b;
|
||||
--color-muted: #94a3b8;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.header-torrent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bitvid-logo {
|
||||
height: 4rem;
|
||||
width: auto;
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.views {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.view-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.view-link:hover {
|
||||
color: var(--color-secondary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
.footer-torrent {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-top: 2rem;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-decoration: none;
|
||||
color: #6b7280;
|
||||
margin: 0 0.25rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--color-secondary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-inline-links {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ipns-text {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.ipns-link {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 95%;
|
||||
max-width: 95%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Table Cells */
|
||||
th,
|
||||
td {
|
||||
padding: 2px 15px;
|
||||
max-width: 200px;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
ul,
|
||||
li {
|
||||
margin-bottom: 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Spinner / Loading overlay */
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 30%;
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
z-index: 1000;
|
||||
background-color: #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
margin-top: -100px;
|
||||
font-size: 200px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Danger (Red) Buttons */
|
||||
.button.button-danger,
|
||||
button.button-danger,
|
||||
input[type="submit"].button-danger,
|
||||
input[type="reset"].button-danger,
|
||||
input[type="button"].button-danger {
|
||||
color: #fff;
|
||||
background-color: #f43f5e;
|
||||
border-color: #f43f5e;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.button.button-danger:hover,
|
||||
button.button-danger:hover,
|
||||
input[type="submit"].button-danger:hover,
|
||||
input[type="reset"].button-danger:hover,
|
||||
input[type="button"].button-danger:hover,
|
||||
.button.button-danger:focus,
|
||||
button.button-danger:focus,
|
||||
input[type="submit"].button-danger:focus,
|
||||
input[type="reset"].button-danger:focus,
|
||||
input[type="button"].button-danger:focus {
|
||||
background-color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Optionally for "Open torrent file" and "Seed files" if using those classes */
|
||||
.open-torrent-btn,
|
||||
.seed-files-btn {
|
||||
color: #bdbdbd;
|
||||
background-color: #f43f5e;
|
||||
border: 1px solid #f43f5e;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
}
|
||||
.open-torrent-btn:hover,
|
||||
.seed-files-btn:hover {
|
||||
background-color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Any other grid or custom layout */
|
||||
.grid {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.download-button {
|
||||
margin-left: 10px;
|
||||
}
|
164
src/torrent/torrent.html
Normal file
164
src/torrent/torrent.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="BTorrent" lang="en">
|
||||
<head>
|
||||
<base href="./" />
|
||||
<meta charset="UTF-8" />
|
||||
<title>bitvid | Browser WebTorrent Client</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="bitvid uses βTorrent. βTorrent is a fully-featured Browser WebTorrent Client"
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="bitvid, client, webtorrent, browser, torrent, stream, bittorrent, torrenting, sharing, filesharing"
|
||||
/>
|
||||
<meta name="author" content="Diego Rodríguez Baquero - DiegoRBaquero" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Scripts: webtorrent, angular, etc. -->
|
||||
<script src="https://cdn.jsdelivr.net/combine/npm/webtorrent/webtorrent.min.js,npm/moment@2,npm/angular@1.5/angular.min.js,npm/angular-route@1.5/angular-route.min.js,npm/angular-sanitize@1.5/angular-sanitize.min.js,npm/angular-ui-grid@3/ui-grid.min.js,gh/matowens/ng-notify/dist/ng-notify.min.js,npm/ng-file-upload@12.2.13/dist/ng-file-upload.min.js"></script>
|
||||
|
||||
<!-- External styles -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/g/normalize@3,skeleton@2,angular.ng-notify@0.8(ng-notify.min.css)"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/fontawesome/4/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/angular.ui-grid/3/ui-grid.min.css"
|
||||
/>
|
||||
|
||||
<!-- Local CSS -->
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body ng-controller="BTorrentCtrl" ng-cloak="">
|
||||
<!-- Header -->
|
||||
<header class="header-torrent">
|
||||
<div class="logo-container">
|
||||
<img
|
||||
src="../assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="bitvid Logo"
|
||||
class="bitvid-logo"
|
||||
/>
|
||||
</div>
|
||||
<!-- Only show links if there's no active torrent -->
|
||||
<div class="views" ng-show="$root.client.torrents.length == 0">
|
||||
<a ng-href="#/" class="view-link">Full</a>
|
||||
<a ng-href="#/download" class="view-link">Single Download</a>
|
||||
<a ng-href="#/view" class="view-link">Stream / View</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Where the 'view.html', 'download.html', or 'full.html' content will appear -->
|
||||
<div id="viewer" ng-style="$root.viewerStyle"></div>
|
||||
<div id="view" ng-view></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer-torrent">
|
||||
<a
|
||||
href="http://bitvid.network/"
|
||||
class="footer-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.network
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="footer-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.eth.limo"
|
||||
class="footer-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.eth.limo
|
||||
</a>
|
||||
|
|
||||
<div class="footer-inline-links">
|
||||
<a
|
||||
href="https://github.com/PR0M3TH3AN/bitvid"
|
||||
class="footer-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
|
||||
class="footer-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
<a
|
||||
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
|
||||
class="footer-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="../getting-started.html"
|
||||
class="footer-link"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
<a
|
||||
href="../about.html"
|
||||
class="footer-link"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="../roadmap.html"
|
||||
class="footer-link"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="torrent.html"
|
||||
class="footer-link"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Torrent
|
||||
</a>
|
||||
</div>
|
||||
<p class="ipns-text">
|
||||
IPNS:
|
||||
<a href="../ipns.html" class="ipns-link">
|
||||
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<!-- Processing Spinner -->
|
||||
<div class="spinner" ng-show="client.processing">
|
||||
<i class="fa fa-spinner fa-spin spinner-icon"></i>
|
||||
</div>
|
||||
|
||||
<!-- Our local Angular code -->
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
140
src/torrent/views/download.html
Normal file
140
src/torrent/views/download.html
Normal file
@@ -0,0 +1,140 @@
|
||||
<div class="container">
|
||||
<!-- Hide this portion if torrents are already added -->
|
||||
<div
|
||||
ng-hide="$root.client.torrents.length != 0"
|
||||
style="vertical-align: middle; text-align: center"
|
||||
>
|
||||
<div class="row">
|
||||
<!-- On submit, call addMagnet() -->
|
||||
<form class="no-margin" ng-submit="addMagnet()">
|
||||
<label>Enter magnet, hash or http(s) .torrent</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="magnet, hash or http(s) .torrent"
|
||||
ng-model="torrentInput"
|
||||
ng-disabled="$root.disabled"
|
||||
style="width: 50%"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>or...</label>
|
||||
<!-- Open torrent file button in red -->
|
||||
<button
|
||||
type="file"
|
||||
ngf-select="$root.openTorrentFile($file)"
|
||||
ng-disabled="$root.disabled"
|
||||
class="button button-danger"
|
||||
>
|
||||
<i class="fa fa-folder-open"></i> Open torrent file
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- If there's a selected torrent, show its info -->
|
||||
<div class="div" ng-if="selectedTorrent" style="text-align: center">
|
||||
<div class="four columns" style="overflow: auto">
|
||||
<h4>Information</h4>
|
||||
<table class="u-full-width">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{$root.selectedTorrent.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{$root.selectedTorrent.length | pbytes}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Completed</td>
|
||||
<td>
|
||||
{{$root.selectedTorrent.progress | progress}}
|
||||
({{$root.selectedTorrent.downloaded | pbytes}})
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Peers</td>
|
||||
<td>{{$root.selectedTorrent.numPeers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>↓ Speed</td>
|
||||
<td>{{$root.selectedTorrent.downloadSpeed | pbytes:1}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ETA</td>
|
||||
<td>{{$root.selectedTorrent.timeRemaining | humanTime}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="four columns">
|
||||
<h4>Files</h4>
|
||||
<table class="u-full-width" style="margin: auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Priority</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="files" ng-repeat="file in $root.selectedTorrent.files">
|
||||
<td ng-hide="file.done">{{file.name}}</td>
|
||||
<td ng-show="file.done">
|
||||
<a
|
||||
ng-href="{{file.url}}"
|
||||
download="{{file.name}}"
|
||||
target="_self"
|
||||
ng-show="file.done"
|
||||
>{{file.name}}</a
|
||||
>
|
||||
</td>
|
||||
<td>{{file.length | pbytes}}</td>
|
||||
<td>
|
||||
<select
|
||||
class="no-margin"
|
||||
name="{{file.name}}Priority"
|
||||
ng-model="file.priority"
|
||||
ng-init="file.priority = '0'"
|
||||
ng-change="$root.changePriority(file)"
|
||||
>
|
||||
<option value="1">High Priority</option>
|
||||
<option value="0" selected="">Low Priority</option>
|
||||
<option value="-1">Don't download</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h5>↑ Click a file to download it</h5>
|
||||
</div>
|
||||
|
||||
<div class="four columns">
|
||||
<h4>Share</h4>
|
||||
<ul style="text-align: justify">
|
||||
<li>
|
||||
<a ng-href="#{{$root.selectedTorrent.infoHash}}" target="_blank"
|
||||
>βTorrent</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-href="{{$root.selectedTorrent.magnetURI}}" target="_blank"
|
||||
>Magnet URI</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
ng-href="{{$root.selectedTorrent.safeTorrentFileURL}}"
|
||||
target="_self"
|
||||
download="{{$root.selectedTorrent.fileName}}"
|
||||
>.torrent</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Hash: </strong>{{$root.selectedTorrent.infoHash}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
159
src/torrent/views/full.html
Normal file
159
src/torrent/views/full.html
Normal file
@@ -0,0 +1,159 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="four columns">
|
||||
<input
|
||||
class="u-full-width"
|
||||
type="text"
|
||||
placeholder="magnet, hash or http(s) .torrent"
|
||||
ng-model="torrentInput"
|
||||
ng-disabled="$root.disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="two columns download-button">
|
||||
<!-- Download button in red -->
|
||||
<button
|
||||
ng-click="addMagnet()"
|
||||
ng-disabled="!torrentInput.length || $root.disabled"
|
||||
class="button button-danger"
|
||||
>
|
||||
<i class="fa fa-download"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="three columns">
|
||||
<!-- Open torrent file button in red -->
|
||||
<button
|
||||
type="file"
|
||||
ngf-select="$root.openTorrentFile($file)"
|
||||
ng-disabled="$root.disabled"
|
||||
class="button button-danger"
|
||||
>
|
||||
<i class="fa fa-folder-open"></i> Open torrent file
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="three columns u-pull-right">
|
||||
<!-- Seed files button in red -->
|
||||
<button
|
||||
class="button button-danger u-pull-right"
|
||||
ngf-select="$root.seedFiles($files)"
|
||||
multiple=""
|
||||
ng-disabled="$root.disabled"
|
||||
>
|
||||
<i class="fa fa-upload"></i> Seed files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row grid"
|
||||
ui-grid="gridOptions"
|
||||
ui-grid-resize-columns="ui-grid-resize-columns"
|
||||
ui-grid-selection="ui-grid-selection"
|
||||
></div>
|
||||
|
||||
<div class="row" ng-if="selectedTorrent">
|
||||
<div class="six columns" style="overflow: auto">
|
||||
<h5>
|
||||
{{$root.selectedTorrent.name}}
|
||||
<!-- Pause/Resume -->
|
||||
<button
|
||||
ng-if="!$root.selectedTorrent.paused"
|
||||
ng-click="$root.selectedTorrent.pause()"
|
||||
>
|
||||
<i class="fa fa-pause"></i> Pause
|
||||
</button>
|
||||
<button
|
||||
ng-if="$root.selectedTorrent.paused"
|
||||
ng-click="$root.selectedTorrent.resume()"
|
||||
>
|
||||
<i class="fa fa-play"></i> Resume
|
||||
</button>
|
||||
<!-- Remove torrent button in red -->
|
||||
<button
|
||||
class="button button-danger"
|
||||
ng-click="$root.selectedTorrent.destroy($root.destroyedTorrent)"
|
||||
>
|
||||
<i class="fa fa-times"></i> Remove
|
||||
</button>
|
||||
</h5>
|
||||
<h6>Share</h6>
|
||||
<ul>
|
||||
<li>
|
||||
<a ng-href="#{{$root.selectedTorrent.infoHash}}" target="_blank"
|
||||
>βTorrent</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-href="{{$root.selectedTorrent.magnetURI}}" target="_blank"
|
||||
>Magnet URI</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
ng-href="{{$root.selectedTorrent.safeTorrentFileURL}}"
|
||||
target="_self"
|
||||
download="{{$root.selectedTorrent.fileName}}"
|
||||
>.torrent</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Hash: </strong>{{$root.selectedTorrent.infoHash}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="six columns">
|
||||
<h5>Files</h5>
|
||||
<table class="u-full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Priority</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="files" ng-repeat="file in $root.selectedTorrent.files">
|
||||
<!-- If file not finished, just show the name -->
|
||||
<td ng-hide="file.done">{{file.name}}</td>
|
||||
<!-- If file is done, show link to download -->
|
||||
<td ng-show="file.done">
|
||||
<a
|
||||
ng-href="{{file.url}}"
|
||||
download="{{file.name}}"
|
||||
target="_self"
|
||||
ng-show="file.done"
|
||||
>{{file.name}}</a
|
||||
>
|
||||
</td>
|
||||
<td>{{file.length | pbytes}}</td>
|
||||
<td>
|
||||
<select
|
||||
class="no-margin"
|
||||
name="{{file.name}}Priority"
|
||||
ng-model="file.priority"
|
||||
ng-init="file.priority = '0'"
|
||||
ng-change="$root.changePriority(file)"
|
||||
>
|
||||
<option value="1">High Priority</option>
|
||||
<option value="0" selected="">Low Priority</option>
|
||||
<option value="-1">Don't download</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<strong>
|
||||
Client Stats:
|
||||
↓ {{$root.client.downloadSpeed | pbytes}}/s ·
|
||||
↑ {{$root.client.uploadSpeed | pbytes}}/s ·
|
||||
Ratio: {{$root.client.ratio | number:2}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
50
src/torrent/views/view.html
Normal file
50
src/torrent/views/view.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<div class="container">
|
||||
<!-- Hide form if there are already torrents in progress -->
|
||||
<div
|
||||
ng-hide="$root.client.torrents.length != 0"
|
||||
style="vertical-align: middle; text-align: center"
|
||||
>
|
||||
<div class="row">
|
||||
<form class="no-margin" ng-submit="addMagnet()">
|
||||
<label>Enter magnet, hash or http(s) .torrent</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="magnet, hash or http(s) .torrent"
|
||||
ng-model="torrentInput"
|
||||
ng-disabled="$root.disabled"
|
||||
style="width: 50%"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>or...</label>
|
||||
<!-- Open torrent file button in red -->
|
||||
<button
|
||||
type="file"
|
||||
ngf-select="$root.openTorrentFile($file)"
|
||||
ng-disabled="$root.disabled"
|
||||
class="button button-danger"
|
||||
>
|
||||
<i class="fa fa-folder-open"></i> Open torrent file
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- If a torrent is selected, show its progress -->
|
||||
<div
|
||||
class="div"
|
||||
ng-if="selectedTorrent"
|
||||
style="text-align: center"
|
||||
>
|
||||
Downloaded
|
||||
{{$root.selectedTorrent.downloaded | pbytes}} /
|
||||
{{$root.selectedTorrent.length | pbytes}}
|
||||
({{$root.selectedTorrent.progress | progress}})
|
||||
at
|
||||
{{$root.selectedTorrent.downloadSpeed | pbytes:1}}
|
||||
from
|
||||
{{$root.selectedTorrent.numPeers}} peers.
|
||||
ETA:
|
||||
{{$root.selectedTorrent.timeRemaining | humanTime}}
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user