From 1c932a122dd098724f7eea104e324a43257738d2 Mon Sep 17 00:00:00 2001 From: Keep Creating Online Date: Sun, 26 Jan 2025 22:48:16 -0500 Subject: [PATCH] =?UTF-8?q?added=20links=20to=20=CE=B2Torrent=20in=20foote?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/about.html | 8 + src/css/torrent-style.css | 96 ------ src/getting-started.html | 8 + src/index.html | 4 +- src/ipns.html | 8 + src/js/torrent-app.js | 531 -------------------------------- src/roadmap.html | 8 + src/torrent.html | 307 ------------------ src/torrent/app.js | 403 ++++++++++++++++++++++++ src/torrent/favicon.ico | Bin 0 -> 15406 bytes src/torrent/style.css | 202 ++++++++++++ src/torrent/torrent.html | 164 ++++++++++ src/torrent/views/download.html | 140 +++++++++ src/torrent/views/full.html | 159 ++++++++++ src/torrent/views/view.html | 50 +++ 15 files changed, 1152 insertions(+), 936 deletions(-) delete mode 100644 src/css/torrent-style.css delete mode 100644 src/js/torrent-app.js delete mode 100644 src/torrent.html create mode 100644 src/torrent/app.js create mode 100644 src/torrent/favicon.ico create mode 100644 src/torrent/style.css create mode 100644 src/torrent/torrent.html create mode 100644 src/torrent/views/download.html create mode 100644 src/torrent/views/full.html create mode 100644 src/torrent/views/view.html diff --git a/src/about.html b/src/about.html index e66f154..05a0e17 100644 --- a/src/about.html +++ b/src/about.html @@ -157,6 +157,14 @@ > Roadmap + + βTorrent +

- File: css/torrent-style.css - Purpose: Additional styles specifically for the Torrent UI queue and table. - */ - -.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; - } \ No newline at end of file diff --git a/src/getting-started.html b/src/getting-started.html index 83b13fa..51c4605 100644 --- a/src/getting-started.html +++ b/src/getting-started.html @@ -157,6 +157,14 @@ > Roadmap + + βTorrent +

- Torrent + βTorrent

Roadmap + + βTorrent +

- File: js/torrent-app.js - Purpose: Recreate core βTorrent-like functionality in a modern design - */ - -// 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 = `Hash: ${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(); - }); \ No newline at end of file diff --git a/src/roadmap.html b/src/roadmap.html index cfe605c..4e8253a 100644 --- a/src/roadmap.html +++ b/src/roadmap.html @@ -138,6 +138,14 @@ > Roadmap + + βTorrent +

- - - - bitvid | Local Torrent Client - - - - - - - - - - - - - - - - - - - - - - - - - - -

- -
-
- - - BitVid Logo - -
-
- - -
-

Download a Torrent

-
- - -
-
- - -
-
- - -
-

Seed Files

- - -
- - - - - - - - - - - - -
- - - - - - - - - - \ No newline at end of file diff --git a/src/torrent/app.js b/src/torrent/app.js new file mode 100644 index 0000000..9c4dfc3 --- /dev/null +++ b/src/torrent/app.js @@ -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(`${file.name} ready for download`, 'success') + } + }) + }) + torrent.on('done', function () { + if (!isSeed) { + dbg('Done', torrent) + ngNotify.set(`${torrent.name} 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)}%` + } +}) diff --git a/src/torrent/favicon.ico b/src/torrent/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8ea244df05532b3e74196c13713369a11cb738e2 GIT binary patch literal 15406 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 T?`#}E(y7P?jUQNGiUs})E2abA literal 0 HcmV?d00001 diff --git a/src/torrent/style.css b/src/torrent/style.css new file mode 100644 index 0000000..386ce85 --- /dev/null +++ b/src/torrent/style.css @@ -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; +} diff --git a/src/torrent/torrent.html b/src/torrent/torrent.html new file mode 100644 index 0000000..9b27ed5 --- /dev/null +++ b/src/torrent/torrent.html @@ -0,0 +1,164 @@ + + + + + + bitvid | Browser WebTorrent Client + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + +
+ + +
+
+ + + + + +
+ +
+ + + + + diff --git a/src/torrent/views/download.html b/src/torrent/views/download.html new file mode 100644 index 0000000..a873b28 --- /dev/null +++ b/src/torrent/views/download.html @@ -0,0 +1,140 @@ +
+ +
+
+ +
+ + +
+
+
+ + + +
+
+ + +
+
+

Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{$root.selectedTorrent.name}}
Size{{$root.selectedTorrent.length | pbytes}}
Completed + {{$root.selectedTorrent.progress | progress}} + ({{$root.selectedTorrent.downloaded | pbytes}}) +
Peers{{$root.selectedTorrent.numPeers}}
↓ Speed{{$root.selectedTorrent.downloadSpeed | pbytes:1}}
ETA{{$root.selectedTorrent.timeRemaining | humanTime}}
+
+ +
+

Files

+ + + + + + + + + + + + + + + + +
NameSizePriority
{{file.name}} + {{file.name}} + {{file.length | pbytes}} + +
+
↑ Click a file to download it
+
+ +
+

Share

+ +
+
+
diff --git a/src/torrent/views/full.html b/src/torrent/views/full.html new file mode 100644 index 0000000..606cdd1 --- /dev/null +++ b/src/torrent/views/full.html @@ -0,0 +1,159 @@ +
+
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+
+ {{$root.selectedTorrent.name}} + + + + + +
+
Share
+ +
+ +
+
Files
+ + + + + + + + + + + + + + + + + + +
NameSizePriority
{{file.name}} + {{file.name}} + {{file.length | pbytes}} + +
+
+
+ +
+ + Client Stats: + ↓ {{$root.client.downloadSpeed | pbytes}}/s · + ↑ {{$root.client.uploadSpeed | pbytes}}/s · + Ratio: {{$root.client.ratio | number:2}} + +
+
diff --git a/src/torrent/views/view.html b/src/torrent/views/view.html new file mode 100644 index 0000000..c479556 --- /dev/null +++ b/src/torrent/views/view.html @@ -0,0 +1,50 @@ +
+ +
+
+
+ + +
+
+
+ + + +
+
+ + +
+ 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}} +
+