fixed bugs in torrent client

This commit is contained in:
Keep Creating Online
2025-01-27 11:14:59 -05:00
parent 7669061e92
commit 7fe5f47a7f
4 changed files with 419 additions and 389 deletions

View File

@@ -1,403 +1,415 @@
/* global WebTorrent, angular, moment, prompt */ /* global WebTorrent, angular, moment */
const VERSION = '1.1' const VERSION = "1.1";
const trackers = ['wss://tracker.btorrent.xyz', 'wss://tracker.openwebtorrent.com']
const rtcConfig = { // A smaller set of WebSocket trackers for reliability.
'iceServers': [ const trackers = [
{ "wss://tracker.btorrent.xyz",
'urls': ['stun:stun.l.google.com:19305', 'stun:stun1.l.google.com:19305'] "wss://tracker.openwebtorrent.com",
} ];
]
// Basic torrent options.
const torrentOpts = { announce: trackers };
const trackerOpts = { announce: trackers };
// Simple debug logger.
function dbg(msg) {
console.log("[DEBUG]", msg);
} }
const torrentOpts = { // Create a WebTorrent client.
announce: trackers const client = new WebTorrent({ tracker: trackerOpts });
}
const trackerOpts = { // Angular app definition.
announce: trackers, const app = angular.module("BTorrent", [
rtcConfig: rtcConfig "ngRoute",
} "ui.grid",
"ui.grid.resizeColumns",
"ui.grid.selection",
"ngFileUpload",
"ngNotify",
]);
const debug = window.localStorage.getItem('debug') !== null /**
* Optional: inline CSS for row lines in the grid.
function dbg (message, item, color = '#333333') { */
if (debug) { const styleEl = document.createElement("style");
if (item && item.name) { styleEl.textContent = `
console.debug( .ui-grid-row {
`%cβTorrent:${item.infoHash ? 'torrent ' : 'torrent ' + item._torrent.name + ':file '}${item.name}${item.infoHash ? ' (' + item.infoHash + ')' : ''} %c${message}`, border-bottom: 1px solid #ccc;
'color: #33C3F0',
`color: ${color}`
)
} else {
console.debug(`%cβTorrent:client %c${message}`, 'color: #33C3F0', `color: ${color}`)
}
} }
} `;
document.head.appendChild(styleEl);
function er (err, item) { // Configure Angular routes.
dbg(err, item, '#FF0000') app.config([
} "$compileProvider",
"$locationProvider",
dbg(`Starting v${VERSION}. WebTorrent ${WebTorrent.VERSION}`) "$routeProvider",
// 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) { function ($compileProvider, $locationProvider, $routeProvider) {
// Allow magnet: and blob: links // Allow magnet, blob, etc. in Angular URLs.
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|magnet|blob|javascript):/) $compileProvider.aHrefSanitizationWhitelist(
/^\s*(https?|magnet|blob|javascript):/
);
$locationProvider.html5Mode(false).hashPrefix("");
// Disable HTML5 mode, only use # routing so no rewrites are needed // Define basic routes.
$locationProvider.html5Mode(false).hashPrefix('')
// Define routes
$routeProvider $routeProvider
.when('/view', { .when("/view", {
templateUrl: 'views/view.html', templateUrl: "views/view.html",
controller: 'ViewCtrl' controller: "ViewCtrl",
}) })
.when('/download', { .when("/download", {
templateUrl: 'views/download.html', templateUrl: "views/download.html",
controller: 'DownloadCtrl' controller: "DownloadCtrl",
}) })
.otherwise({ .otherwise({
templateUrl: 'views/full.html', templateUrl: "views/full.html",
controller: 'FullCtrl' controller: "FullCtrl",
}) });
} },
]) ]);
app.controller('BTorrentCtrl', [ // Warn user before they unload if torrents are still active.
'$scope', app.run([
'$rootScope', "$rootScope",
'$http', function ($rootScope) {
'$log', window.addEventListener("beforeunload", (e) => {
'$location', if ($rootScope.client && $rootScope.client.torrents.length > 0) {
'ngNotify', e.preventDefault();
function ($scope, $rootScope, $http, $log, $location, ngNotify) { e.returnValue =
$rootScope.version = VERSION "Transfers are in progress. Are you sure you want to leave or refresh?";
$rootScope.webtorrentVersion = WebTorrent.VERSION return e.returnValue;
}
});
},
]);
ngNotify.config({ // Main BTorrent controller.
duration: 5000, app.controller("BTorrentCtrl", [
html: true "$scope",
}) "$rootScope",
"$http",
"$log",
"ngNotify",
function ($scope, $rootScope, $http, $log, ngNotify) {
dbg("Starting app.js version " + VERSION);
if (!WebTorrent.WEBRTC_SUPPORT) { if (!WebTorrent.WEBRTC_SUPPORT) {
$rootScope.disabled = true ngNotify.set("Please use a browser with WebRTC support.", "error");
ngNotify.set('Please use a WebRTC compatible browser', {
type: 'error',
sticky: true,
button: false
})
} }
$rootScope.client = client $rootScope.client = client;
$rootScope.selectedTorrent = null;
$rootScope.processing = false;
function updateAll () { // Global error handler.
if (!$rootScope.client.processing) { client.on("error", (err) => {
$rootScope.$applyAsync() dbg("Torrent client error: " + err);
} ngNotify.set(err.message || err, "error");
} $rootScope.processing = false;
});
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)
}
}
/**
* Called whenever a new torrent is added or we seed files.
*/
$rootScope.onTorrent = function (torrent, isSeed) { $rootScope.onTorrent = function (torrent, isSeed) {
dbg(torrent.magnetURI) dbg("Torrent added: " + torrent.magnetURI);
torrent.safeTorrentFileURL = torrent.torrentFileBlobURL $rootScope.processing = false;
torrent.fileName = `${torrent.name}.torrent`
if (!isSeed) { if (!isSeed) {
dbg('Received metadata', torrent) ngNotify.set(`Received ${torrent.name} metadata`);
ngNotify.set(`Received ${torrent.name} metadata`)
if (!$rootScope.selectedTorrent) {
$rootScope.selectedTorrent = torrent
}
$rootScope.client.processing = false
} }
torrent.files.forEach(function (file) { if (!$rootScope.selectedTorrent) {
file.getBlobURL(function (err, url) { $rootScope.selectedTorrent = torrent;
if (err) throw err }
file.url = url
if (isSeed) { // Generate file.blobURL for direct downloading in the file table.
dbg('Started seeding', torrent) torrent.files.forEach((file) => {
if (!$rootScope.selectedTorrent) { file.getBlobURL((err, url) => {
$rootScope.selectedTorrent = torrent if (!err) {
} file.url = url;
$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') * Add a magnet link or .torrent URL.
*/
$rootScope.addMagnet = function (magnet) {
if (!magnet) return;
$rootScope.processing = true;
dbg("Adding magnet: " + magnet);
client.add(magnet, torrentOpts, (torrent) => {
$rootScope.onTorrent(torrent, false);
$scope.$applyAsync();
});
};
/**
* Seed local files.
*/
$rootScope.seedFiles = function (files) {
if (!files || !files.length) return;
$rootScope.processing = true;
dbg(`Seeding ${files.length} file(s)`);
client.seed(files, torrentOpts, (torrent) => {
$rootScope.onTorrent(torrent, true);
$scope.$applyAsync();
});
};
/**
* Remove/destroy a selected torrent.
*/
$rootScope.destroyedTorrent = function (err) {
if (err) {
console.error("Failed to destroy torrent:", err);
}
dbg("Destroyed torrent", $rootScope.selectedTorrent);
$rootScope.selectedTorrent = null;
$rootScope.processing = false;
};
/**
* Change the priority of an individual file (high, low, or don't download).
*/
$rootScope.changePriority = function (file) {
if (file.priority === "-1") {
file.deselect();
dbg("Deselected file", file);
} else {
file.select(file.priority);
dbg(`Selected with priority ${file.priority}`, file);
}
};
/**
* Download all files in the selected torrent if it's 100% done.
* Creates a blob for each file, triggers a native download with <a>.
*/
$rootScope.downloadAll = function (torrent) {
if (!torrent) return;
if (torrent.progress < 1) {
alert("Torrent is not finished downloading yet.");
return;
}
torrent.files.forEach((file) => {
file.getBlob((err, blob) => {
if (err) {
console.error("Failed to get blob for file:", file.name, err);
return;
}
// Create an anchor to trigger the download.
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = blobUrl;
a.download = file.name;
// Append, click, remove, revoke.
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
});
});
};
/**
* Copy the magnet URI of a torrent to the clipboard.
*/
$rootScope.copyMagnetURI = function (torrent) {
if (!torrent || !torrent.magnetURI) return;
const magnetURI = torrent.magnetURI;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(magnetURI)
.then(() =>
ngNotify.set("Magnet URI copied to clipboard!", "success")
)
.catch((err) => {
console.error("Clipboard error:", err);
ngNotify.set("Failed to copy magnet URI", "error");
});
} else {
try {
const textarea = document.createElement("textarea");
textarea.value = magnetURI;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
ngNotify.set("Magnet URI copied (fallback)!", "success");
} catch (err) {
console.error("Clipboard fallback error:", err);
ngNotify.set("Failed to copy magnet URI", "error");
} }
}) }
torrent.on('wire', function (wire, addr) { };
dbg(`Wire ${addr}`, torrent)
})
torrent.on('error', er)
}
$rootScope.onSeed = function (torrent) { /**
$rootScope.onTorrent(torrent, true) * Save the .torrent file itself (via torrent.torrentFileBlobURL).
} */
$rootScope.saveTorrentFile = function (torrent) {
if (!torrent || !torrent.torrentFileBlobURL) return;
const fileName = torrent.fileName || `${torrent.name}.torrent`;
// Create a hidden <a> to force download of the .torrent file.
const a = document.createElement("a");
a.style.display = "none";
a.href = torrent.torrentFileBlobURL;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
},
]);
dbg('Angular ready') // FullCtrl: sets up the ui-grid and magnet input for /full route.
} app.controller("FullCtrl", [
]) "$scope",
"$rootScope",
// Full View Controller "$location",
app.controller('FullCtrl', [ "ngNotify",
'$scope', function ($scope, $rootScope, $location, ngNotify) {
'$rootScope', // Handle magnet input
'$http',
'$log',
'$location',
'ngNotify',
function ($scope, $rootScope, $http, $log, $location, ngNotify) {
ngNotify.config({
duration: 5000,
html: true
})
$scope.addMagnet = function () { $scope.addMagnet = function () {
$rootScope.addMagnet($scope.torrentInput) $rootScope.addMagnet($scope.torrentInput);
$scope.torrentInput = '' $scope.torrentInput = "";
};
// If we have a #magnet in the URL, add it automatically
if ($location.hash()) {
$rootScope.addMagnet($location.hash());
} }
// Define columns for ui-grid.
$scope.columns = [ $scope.columns = [
{ field: 'name', cellTooltip: true, minWidth: 200 }, { field: "name", displayName: "Name", minWidth: 200 },
{ field: 'length', name: 'Size', cellFilter: 'pbytes', width: 80 }, {
{ field: 'received', displayName: 'Downloaded', cellFilter: 'pbytes', width: 135 }, field: "progress",
{ field: 'downloadSpeed', displayName: '↓ Speed', cellFilter: 'pbytes:1', width: 100 }, displayName: "Progress",
{ field: 'progress', displayName: 'Progress', cellFilter: 'progress', width: 100 }, cellFilter: "progress",
{ field: 'timeRemaining', displayName: 'ETA', cellFilter: 'humanTime', width: 140 }, width: 100,
{ field: 'uploaded', displayName: 'Uploaded', cellFilter: 'pbytes', width: 125 }, },
{ field: 'uploadSpeed', displayName: '↑ Speed', cellFilter: 'pbytes:1', width: 100 }, {
{ field: 'numPeers', displayName: 'Peers', width: 80 }, field: "downloadSpeed",
{ field: 'ratio', cellFilter: 'number:2', width: 80 } displayName: "↓ Speed",
] cellFilter: "pbytes:1",
width: 100,
},
{
field: "uploadSpeed",
displayName: "↑ Speed",
cellFilter: "pbytes:1",
width: 100,
},
{ field: "numPeers", displayName: "Peers", width: 80 },
{
field: "ratio",
displayName: "Ratio",
cellFilter: "number:2",
width: 80,
},
];
// Create gridOptions and update each second.
$scope.gridOptions = { $scope.gridOptions = {
columnDefs: $scope.columns, columnDefs: $scope.columns,
data: $rootScope.client.torrents,
enableColumnResizing: true, enableColumnResizing: true,
enableColumnMenus: false, enableColumnMenus: false,
enableRowSelection: true, enableRowSelection: true,
enableRowHeaderSelection: false, enableRowHeaderSelection: false,
multiSelect: false multiSelect: false,
} data: [],
};
setInterval(() => {
$scope.gridOptions.data =
($rootScope.client && $rootScope.client.torrents) || [];
$scope.$applyAsync();
}, 1000);
// On row selection, set the selectedTorrent
$scope.gridOptions.onRegisterApi = function (gridApi) { $scope.gridOptions.onRegisterApi = function (gridApi) {
$scope.gridApi = gridApi $scope.gridApi = gridApi;
gridApi.selection.on.rowSelectionChanged($scope, function (row) { gridApi.selection.on.rowSelectionChanged($scope, function (row) {
if (!row.isSelected && $rootScope.selectedTorrent && $rootScope.selectedTorrent.infoHash === row.entity.infoHash) { if (row.isSelected) {
$rootScope.selectedTorrent = null $rootScope.selectedTorrent = row.entity;
} else { } else {
$rootScope.selectedTorrent = row.entity $rootScope.selectedTorrent = null;
} }
}) });
} };
},
// 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
})
// DownloadCtrl / ViewCtrl are minimal in this example.
app.controller("DownloadCtrl", [
"$scope",
"$rootScope",
"$location",
function ($scope, $rootScope, $location) {
$scope.addMagnet = function () { $scope.addMagnet = function () {
$rootScope.addMagnet($scope.torrentInput) $rootScope.addMagnet($scope.torrentInput);
$scope.torrentInput = '' $scope.torrentInput = "";
} };
if ($location.hash()) {
if ($location.hash() !== '') { $rootScope.addMagnet($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)
} }
},
]);
app.controller("ViewCtrl", [
"$scope",
"$rootScope",
"$location",
function ($scope, $rootScope, $location) {
$scope.addMagnet = function () { $scope.addMagnet = function () {
$rootScope.addMagnet($scope.torrentInput, onTorrent) $rootScope.addMagnet($scope.torrentInput);
$scope.torrentInput = '' $scope.torrentInput = "";
};
if ($location.hash()) {
$rootScope.addMagnet($location.hash());
} }
},
]);
// If there's a magnet in the URL // Angular filters for size, progress, etc.
if ($location.hash() !== '') { app.filter("pbytes", function () {
$rootScope.client.processing = true return function (num, speed) {
setTimeout(function () { if (isNaN(num)) return "";
dbg(`Adding ${$location.hash()}`) if (num < 1) return speed ? "" : "0 B";
$rootScope.addMagnet($location.hash(), onTorrent) const units = ["B", "kB", "MB", "GB", "TB"];
}, 0) const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), 4);
} const val = (num / Math.pow(1000, exponent)).toFixed(1) * 1;
} return `${val} ${units[exponent]}${speed ? "/s" : ""}`;
]) };
});
// Custom Angular filters app.filter("progress", function () {
app.filter('html', [ return function (val) {
'$sce', if (typeof val !== "number") return "";
return (val * 100).toFixed(1) + "%";
};
});
app.filter("html", [
"$sce",
function ($sce) { function ($sce) {
return function (input) { return function (input) {
return $sce.trustAsHtml(input) return $sce.trustAsHtml(input);
} };
} },
]) ]);
app.filter('pbytes', function () { app.filter("humanTime", 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) { return function (millis) {
if (millis < 1000) return '' if (millis < 1000) return "";
const remaining = moment.duration(millis).humanize() const duration = moment.duration(millis).humanize();
return remaining.charAt(0).toUpperCase() + remaining.slice(1) return duration.charAt(0).toUpperCase() + duration.slice(1);
} };
}) });
app.filter('progress', function () {
return function (num) {
return `${(100 * num).toFixed(1)}%`
}
})

View File

@@ -40,10 +40,11 @@ body {
} }
.bitvid-logo { .bitvid-logo {
height: 4rem; height: 6rem;
width: auto; width: auto;
/* Adjust spacing around the logo if needed */ /* Adjust spacing around the logo if needed */
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
margin-top: 4rem;
} }
/* Footer styling */ /* Footer styling */

View File

@@ -18,6 +18,7 @@
<!-- Scripts: webtorrent, angular, etc. --> <!-- Scripts: webtorrent, angular, etc. -->
<script src="bundle.min.js"></script> <script src="bundle.min.js"></script>
<!--<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 --> <!-- External styles -->
<link <link

View File

@@ -1,4 +1,6 @@
<!-- MAIN CONTENT AREA -->
<div class="container"> <div class="container">
<!-- Input row for adding or opening torrents -->
<div class="row"> <div class="row">
<div class="four columns"> <div class="four columns">
<input <input
@@ -11,7 +13,7 @@
</div> </div>
<div class="two columns download-button"> <div class="two columns download-button">
<!-- Download button in red --> <!-- "Download" button for adding a magnet/torrent -->
<button <button
ng-click="addMagnet()" ng-click="addMagnet()"
ng-disabled="!torrentInput.length || $root.disabled" ng-disabled="!torrentInput.length || $root.disabled"
@@ -22,7 +24,7 @@
</div> </div>
<div class="three columns"> <div class="three columns">
<!-- Open torrent file button in red --> <!-- "Open torrent file" button -->
<button <button
type="file" type="file"
ngf-select="$root.openTorrentFile($file)" ngf-select="$root.openTorrentFile($file)"
@@ -34,11 +36,11 @@
</div> </div>
<div class="three columns u-pull-right"> <div class="three columns u-pull-right">
<!-- Seed files button in red --> <!-- "Seed files" button -->
<button <button
class="button button-danger u-pull-right" class="button button-danger u-pull-right"
ngf-select="$root.seedFiles($files)" ngf-select="$root.seedFiles($files)"
multiple="" multiple="true"
ng-disabled="$root.disabled" ng-disabled="$root.disabled"
> >
<i class="fa fa-upload"></i> Seed files <i class="fa fa-upload"></i> Seed files
@@ -46,6 +48,7 @@
</div> </div>
</div> </div>
<!-- ui-grid for active torrents (Name, Progress, Speeds, Peers, Ratio) -->
<div <div
class="row grid" class="row grid"
ui-grid="gridOptions" ui-grid="gridOptions"
@@ -53,52 +56,65 @@
ui-grid-selection="ui-grid-selection" ui-grid-selection="ui-grid-selection"
></div> ></div>
<!-- Selected torrent details: Pause/Resume, Download, Remove -->
<div class="row" ng-if="selectedTorrent"> <div class="row" ng-if="selectedTorrent">
<div class="six columns" style="overflow: auto"> <div class="six columns" style="overflow: auto">
<h5> <h5>
{{$root.selectedTorrent.name}} {{ selectedTorrent.name }}
<!-- Pause/Resume -->
<!-- Pause/Resume buttons -->
<button <button
ng-if="!$root.selectedTorrent.paused" ng-if="!selectedTorrent.paused"
ng-click="$root.selectedTorrent.pause()" ng-click="selectedTorrent.pause()"
> >
<i class="fa fa-pause"></i> Pause <i class="fa fa-pause"></i> Pause
</button> </button>
<button <button
ng-if="$root.selectedTorrent.paused" ng-if="selectedTorrent.paused"
ng-click="$root.selectedTorrent.resume()" ng-click="selectedTorrent.resume()"
> >
<i class="fa fa-play"></i> Resume <i class="fa fa-play"></i> Resume
</button> </button>
<!-- Remove torrent button in red -->
<!-- Download ALL files (only if progress == 1) -->
<button <button
class="button button-danger" class="button button-danger"
ng-click="$root.selectedTorrent.destroy($root.destroyedTorrent)" ng-click="$root.downloadAll(selectedTorrent)"
ng-disabled="selectedTorrent.progress < 1"
>
<i class="fa fa-download"></i> Download
</button>
<!-- Remove button -->
<button
class="button button-danger"
ng-click="selectedTorrent.destroy($root.destroyedTorrent)"
> >
<i class="fa fa-times"></i> Remove <i class="fa fa-times"></i> Remove
</button> </button>
</h5> </h5>
<h6>Share</h6> <h6>Share</h6>
<ul> <ul>
<li> <li>
<a ng-href="#{{$root.selectedTorrent.infoHash}}" target="_blank" <!-- Copy magnet button -->
>bitvid</a <button
class="button button-small"
ng-click="$root.copyMagnetURI(selectedTorrent)"
> >
Copy Magnet
</button>
</li> </li>
<li> <li>
<a ng-href="{{$root.selectedTorrent.magnetURI}}" target="_blank" <!-- Download .torrent file button -->
>Magnet URI</a <button
class="button button-small"
ng-click="$root.saveTorrentFile(selectedTorrent)"
> >
Download .torrent
</button>
</li> </li>
<li> <li><strong>Hash:</strong> {{ selectedTorrent.infoHash }}</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> </ul>
</div> </div>
@@ -113,24 +129,20 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr class="files" ng-repeat="file in $root.selectedTorrent.files"> <tr class="files" ng-repeat="file in selectedTorrent.files">
<!-- If file not finished, just show the name --> <!-- If file isn't finished, just show the name. -->
<td ng-hide="file.done">{{file.name}}</td> <td ng-hide="file.done">{{ file.name }}</td>
<!-- If file is done, show link to download --> <!-- If file is done, show direct link to download it. -->
<td ng-show="file.done"> <td ng-show="file.done">
<a <a ng-href="{{ file.url }}" download="{{ file.name }}">
ng-href="{{file.url}}" {{ file.name }}
download="{{file.name}}" </a>
target="_self"
ng-show="file.done"
>{{file.name}}</a
>
</td> </td>
<td>{{file.length | pbytes}}</td> <td>{{ file.length | pbytes }}</td>
<td> <td>
<select <select
class="no-margin" class="no-margin"
name="{{file.name}}Priority" name="{{ file.name }}Priority"
ng-model="file.priority" ng-model="file.priority"
ng-init="file.priority = '0'" ng-init="file.priority = '0'"
ng-change="$root.changePriority(file)" ng-change="$root.changePriority(file)"
@@ -148,9 +160,13 @@
<div class="center"> <div class="center">
<strong> <strong>
Client Stats: ↓ {{$root.client.downloadSpeed | pbytes}}/s · ↑ Client Stats: ↓ {{ client.downloadSpeed | pbytes }}/s · ↑ {{
{{$root.client.uploadSpeed | pbytes}}/s · Ratio: {{$root.client.ratio | client.uploadSpeed | pbytes }}/s · Ratio: {{ client.ratio | number:2 }}
number:2}}
</strong> </strong>
</div> </div>
</div> </div>
<!-- Processing Spinner -->
<div class="spinner" ng-show="client.processing">
<i class="fa fa-spinner fa-spin spinner-icon"></i>
</div>