diff -U2 -r /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/index.html /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/index.html
--- /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/index.html 2026-10-28 22:59:59.000000000 +0000
+++ /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/index.html 2026-10-28 22:33:51.000000000 +0000
@@ -49,5 +49,5 @@
.torrent-row:hover td {
- background-color: #ffe6008e;
+ background-color: #ff9900ce;
}
@@ -69,5 +69,5 @@
}
-.buttons-cell {
+.buttons-cell .button-row {
display: flex;
flex-direction: row;
@@ -85,5 +85,7 @@
}
-.buttons-cell button {
+.buttons-cell button,
+.buttons-column button,
+.button-row button {
flex: 1;
margin: 0;
@@ -94,4 +96,8 @@
}
+.button-row button:hover {
+ background-color: orange;
+}
+
.fs-buttons {
width: 100%;
@@ -108,41 +114,10 @@
min-width: 0;
}
-
-.torrent-rcm {
- position: fixed;
- z-index: 10000;
- min-width: 100px;
- background: #fff;
- color: #000;
- box-shadow: 0 4px 12px #0009;
- border: 1px solid rgb(122, 122, 122);
- font-size: 14px;
-}
-
-.torrent-rcm[hidden] {
- display: none;
-}
-
-.torrent-rcm [data-action] {
- padding: 1px 4px;
- cursor: pointer;
-}
-
-.torrent-rcm [data-action]:hover {
- background: #e8e8e8;
-}
-
-.torrent-rcm hr {
- border: 0;
- border-top: 1px solid #ddd;
-}
-
-
@@ -152,5 +127,5 @@
-
+
diff -U2 -r /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/main.js /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/main.js
--- /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/main.js 2026-10-28 22:59:59.000000000 +0000
+++ /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/main.js 2026-10-28 22:33:51.000000000 +0000
@@ -1,76 +1,19 @@
-// torrents from torrent-get
-let torrents = [];
+let torrents = []; // torrents table data
+let details_id = null; // torrent id for details table
-// torrent id for openTorrentDetails() selector
-let selected_torrent = 0;
+const el_torrents = document.getElementById("div-torrents")
-// selection of torrents from refreshTable() for actions
-let selected_torrents = [];
+const el_fs_error = document.getElementById("fs-error")
+const el_err_string = document.getElementById("err-string");
+const el_err_details = document.getElementById("err-details")
-// update delays
-let torrents_delay = 5*1000 // 5 sec
-let stats_delay = 10*1000 // 10 sec
+const el_details = document.getElementById("div-details")
+const el_torrentfile = document.getElementById("torrent-file")
-const el = {
- torrents: document.getElementById("div-torrents"),
+const el_dht_stats_list = document.getElementById("dht-stats-list")
+const el_fs_dht_stats = document.getElementById("fs-dht-stats");
- fs_error: document.getElementById("fs-error"),
- err_string: document.getElementById("err-string"),
- err_details: document.getElementById("err-details"),
-
- details: document.getElementById("div-details"),
- torrent_file: document.getElementById("torrent-file"),
-
- dht_stats_list: document.getElementById("dht-stats-list"),
- fs_dht_stats: document.getElementById("fs-dht-stats"),
-
- session_stats_list: document.getElementById("session-stats-list"),
- fs_session_stats: document.getElementById("fs-session-stats"),
-
- torrent_rcm: document.getElementById("torrent-rcm")
-};
-
-function now_ts() { return Math.floor(Date.now() / 1000) };
-let exh_ts = 0;
-
-// display errors
-function vis_exh(err = "", err_details = "") {
- const is_blank = !(err || err_details);
-
- if (is_blank && now_ts() - exh_ts < 30)
- return;
-
- exh_ts = now_ts()
-
- el.err_string.textContent = err;
- el.err_details.textContent = err_details;
- el.fs_error.style.display = is_blank ? "none" : "block";
- if (!is_blank) {
- console.log(err)
- console.log(err_details)
- }
-}
-
-// catch js errors
-window.addEventListener("error", event => {
- vis_exh(
- `[js] ${event.message}`,
- `${event.filename}@${event.lineno}:${event.colno}: ${event.error}`
- );
-});
-
-// catch async errors
-window.addEventListener("unhandledrejection", event => {
- const error = event.reason;
- vis_exh(
- `[promise] ${error}`,
- error?.stack
- )
-});
-
-// checking for loaded js files
-function js() {
- return Boolean(window.rpc && window.util)
-}
+const el_session_stats_list = document.getElementById("session-stats-list")
+const el_fs_session_stats = document.getElementById("fs-session-stats")
refreshTable();
@@ -83,479 +26,250 @@
refreshSessionStats()
]);
-}, stats_delay);
-
-setInterval(
- async () => refreshTable(),
- torrents_delay
-);
-
-async function addMagnet() {
- const input = prompt("enter the magnet link:", "");
+}, 10000);
- if (!input)
- return;
-
- const magnets = input.split(" ");
-
- magnets.forEach(async magnet => {
- const r = await rpc.addTorrentByMagnet(magnet)
- if (magnets.length === 1 && r.result !== "success")
- alert(r.result);
- });
+setInterval(async () => refreshTable(), 5000);
- await refreshTable();
+function jsLoaded() {
+ return window.rpc && window.utils
}
-async function torrentRemove(ids = [], withFiles = false) {
- if (!js() || !ids.length) return;
-
- let warn = `remove the following torrents${withFiles ? ' WITH FILES' : ""}?\n\n`
+function setError(err, err_details) {
+ el_err_string.textContent = err || "";
+ el_err_details.textContent = err_details || "";
+ el_fs_error.style.display = "block";
+ if (err_details)
+ console.log(err_details)
+}
- const objs = util.sel.idsToObj(ids)
+function clearError() {
+ setError("", "")
+ el_fs_error.style.display = "none";
+}
- objs.forEach(obj => {
- warn += `#${obj.id} | ${obj.name}\n`
- });
+async function addMagnet() {
+ const name = prompt("enter the magnet link:", "");
- const confirmed = confirm(warn);
- if (!confirmed)
+ if (name === null)
return;
- await rpc.torrentRemove(ids, withFiles);
- await refreshTable({
- closeDetails:
- selected_torrent === ids[0] || util.sel.isSelected(selected_torrent)
- });
-};
-
-// add local torrent files
-el.torrent_file.onchange = async (e) => {
- if (!js()) return;
-
- const input = e.target;
- const files = [...input.files];
+ const r = await rpc.addTorrentByMagnet(name)
+ if (r.result !== "success")
+ alert(r.result);
+ refreshTable();
+}
- if (!files || !files.length)
+async function stopAll() {
+ if (!jsLoaded())
return;
- await Promise.all(files.map(async file => {
- const bytes = new Uint8Array(await file.arrayBuffer());
-
- const b64 = btoa(
- Array.from(bytes, byte => String.fromCharCode(byte)).join("")
- );
-
- await rpc.addTorrent(b64);
- }));
-
- input.value = "";
- await refreshTable();
-};
-
-// open details for selected row
-el.torrents.addEventListener("click", async event => {
- if (!js()) return;
+ const r = await rpc.torrentStop()
+ if (r.result !== "success")
+ alert(r.result);
+}
- if (event.target.closest("a, input, select"))
+async function startAll() {
+ if (!jsLoaded())
return;
- const row = event.target.closest("tr");
- if (!row || !el.torrents.contains(row))
- return;
+ const r = await rpc.torrentStart()
+ if (r.result !== "success")
+ alert(r.result);
+}
- const id = +row.dataset.id;
- if (!id)
+async function removeTorrent(id) {
+ if (!jsLoaded())
return;
- const ctrl = event.ctrlKey || event.metaKey
- const shift = event.shiftKey
-
- if (shift) {
- // range selecting
- event.preventDefault();
- const arr = util.sel.getAllIDs();
-
- const a = arr.indexOf(selected_torrent);
- const b = arr.indexOf(id);
- const r = arr.slice(
- Math.min(a, b),
- Math.max(a, b) + 1
- );
-
- ctrl ? util.sel.deselectIDs(r) : util.sel.selectIDs(r);
-
- } else if (ctrl) {
- // single selecting
- event.preventDefault();
- util.sel.toggleID(id)
- }
-
- selected_torrent = id;
-
- await refreshTable({ fetchTorrents: false });
-});
-
-// prevent text from being selected when a range is selected
-el.torrents.addEventListener("mousedown", event => {
- if (event.shiftKey)
- event.preventDefault();
-});
-
-// rcm (right-click-menu) for torrent table
-el.torrents.addEventListener("contextmenu", async event => {
- const row = event.target.closest("tr");
+ const confirmed = confirm(`remove the torrent with id=${id} ?`);
+ if (confirmed)
+ await rpc.removeTorrent(Number(id), false);
+};
- if (!row || !el.torrents.contains(row))
+async function removeTorrentAndFiles(id) {
+ if (!jsLoaded())
return;
- const id = +row.dataset.id;
- if (!id)
- return;
+ const confirmed = confirm(`remove the torrent AND FILES with id=${id} ?`);
+ if (confirmed)
+ await rpc.removeTorrent(Number(id), true);
+};
- const torrent = torrents.find(t => t.id == id);
- if (!torrent)
+// add local torrent file
+el_torrentfile.onchange = (e) => {
+ if (!jsLoaded())
return;
- const running = torrent.status > 0
-
- event.preventDefault();
-
- selected_torrent = id;
- refreshTable({ fetchTorrents: false });
-
- // cleaning rcm
- el.torrent_rcm.replaceChildren();
-
- if (util.sel.isEnabled()) {
- if (util.sel.isSomeStopped()) {
- const start = document.createElement("div");
- start.dataset.action = "start";
- start.textContent = "βΆοΈ Start selected";
- el.torrent_rcm.append(start);
- }
-
- if (util.sel.isSomeRunning()) {
- const pause = document.createElement("div");
- pause.dataset.action = "pause";
- pause.textContent = "βΈοΈ Pause selected";
- el.torrent_rcm.append(pause);
-
- const reannounce = document.createElement("div");
- reannounce.dataset.action = "reannounce";
- reannounce.textContent = "π Reannounce selected";
- el.torrent_rcm.append(reannounce);
- }
- }
- else {
- const setStatus = document.createElement("div");
- setStatus.dataset.action = running ? "pause" : "start";
- setStatus.textContent = running ? "βΈοΈ Pause" : "βΆοΈ Start";
- el.torrent_rcm.append(setStatus);
-
- if (running) {
- const reannounce = document.createElement("div");
- reannounce.dataset.action = "reannounce";
- reannounce.textContent = "π Reannounce";
- el.torrent_rcm.append(reannounce);
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = async () => {
+ const bytes = new Uint8Array(reader.result);
+ let binary = "";
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
}
- }
-
- // separator
- el.torrent_rcm.append(document.createElement("hr"));
-
- const toggle = document.createElement("div");
- toggle.dataset.action = "toggle";
- toggle.textContent = util.sel.isSelected(id)
- ? "β Unselect"
- : "β
Select";
- el.torrent_rcm.append(toggle);
-
- const toggle_all = document.createElement("div");
- toggle_all.dataset.action = "toggle_all";
- toggle_all.textContent = util.sel.allChecked()
- ? "β Unselect all"
- : "β
Select all";
- el.torrent_rcm.append(toggle_all);
-
- // separator
- el.torrent_rcm.append(document.createElement("hr"));
+ const b64 = btoa(binary);
+ await rpc.addTorrent(b64);
+ e.target.value = "";
+ };
+ reader.readAsArrayBuffer(file);
- // delete .torrent
- const magnet = document.createElement("div");
- magnet.dataset.action = "magnet";
- magnet.textContent = util.sel.isEnabled()
- ? "π§² Copy magnet links"
- : "π§² Copy magnet link";
- el.torrent_rcm.append(magnet);
-
- // separator
- el.torrent_rcm.append(document.createElement("hr"));
-
- // delete .torrent
- const delete_ = document.createElement("div");
- delete_.dataset.action = "delete";
- delete_.textContent = util.sel.isEnabled()
- ? "ποΈ Delete selected"
- : "ποΈ Delete";
- el.torrent_rcm.append(delete_);
-
- // delete .torrent and downloaded files
- const deletefiles = document.createElement("div");
- deletefiles.dataset.action = "delete_files";
- deletefiles.textContent = util.sel.isEnabled()
- ? "β Delete selected with files"
- : "β Delete with files";
- el.torrent_rcm.append(deletefiles);
-
- // move the menu directly into the body
- document.body.appendChild(el.torrent_rcm);
-
- el.torrent_rcm.hidden = false;
- el.torrent_rcm.style.position = "fixed";
- el.torrent_rcm.style.left = `${event.clientX}px`;
- el.torrent_rcm.style.top = `${event.clientY}px`;
+ refreshTable();
+};
- // adjust the position if the menu extends beyond the window's boundaries
- const rect = el.torrent_rcm.getBoundingClientRect();
+async function refreshSessionStats() {
+ if (!jsLoaded())
+ return;
- if (rect.right > window.innerWidth - 5)
- el.torrent_rcm.style.left = `${Math.max(5, window.innerWidth - rect.width - 5)}px`;
+ const r = await rpc.sessionStats();
+
+ if (!r?.arguments?.["current-stats"])
+ return (el_fs_session_stats.style.display = "none")
- if (rect.bottom > window.innerHeight - 5)
- el.torrent_rcm.style.top = `${Math.max(5, window.innerHeight - rect.height - 5)}px`;
-});
+ result = r.arguments["current-stats"]
-// performing the selected rcm action
-el.torrent_rcm.addEventListener("click", async event => {
- const item = event.target.closest("[data-action]");
- if (!item || !el.torrent_rcm.contains(item))
- return;
+ const list = document.createElement("div");
- const torrentActions = {
- start: async ids => rpc.torrentStart(ids),
- pause: async ids => rpc.torrentStop(ids),
- reannounce: async ids => rpc.torrentReannounce(ids),
- delete: async ids => torrentRemove(ids, false),
- delete_files: async ids => torrentRemove(ids, true),
- toggle: ids => util.sel.toggleID(selected_torrent),
- toggle_all: ids => util.sel.toggleAll(),
- magnet: ids => util.copy.magnets( util.sel.idsToObj(ids) )
+ const labels = {
+ downloadedBytes: "Downloaded",
+ uploadedBytes: "Uploaded"
};
- const action = torrentActions[item.dataset.action];
- if (!action || !selected_torrent || !js())
- return;
+ Object.keys(result).forEach(key => {
+ if (key in labels) {
+ const cell = document.createElement("li");
+ cell.textContent = `${labels[key]}: ${utils.formatSize(result[key])}`;
+ list.appendChild(cell);
+ }
+ });
- el.torrent_rcm.hidden = true;
+ el_session_stats_list.replaceChildren(list)
+ el_fs_session_stats.style.display = "block";
+}
- action(selected_torrents.length ? selected_torrents : [selected_torrent]);
+async function refreshDhtStats() {
+ if (!jsLoaded())
+ return;
- let fetchTorrents = false
- if (['start', 'pause', 'reannounce'].includes(item.dataset.action)) {
- util.sel.deselectAll();
- fetchTorrents = true
- }
+ const r = await rpc.dhtStats();
+
+ if (!r?.arguments)
+ return (el_fs_dht_stats.style.display = "none")
- await refreshTable({ fetchTorrents: fetchTorrents });
-});
+ result = r.arguments
-// close the rcm by clicking outside
-document.addEventListener("click", event => {
- if (!el.torrent_rcm.contains(event.target))
- el.torrent_rcm.hidden = true;
-});
+ const list = document.createElement("div");
-// hotkeys
-document.addEventListener("keydown", event => {
- const ids = selected_torrents.length ? selected_torrents : [selected_torrent]
-
- const up = event.key === "ArrowUp" || event.key === "ArrowLeft"
- const down = event.key === "ArrowRight" || event.key === "ArrowDown"
-
- const esc = event.key === "Escape"
- const space = event.code === "Space"
- const shift = event.shiftKey
- const ctrl = event.ctrlKey || event.metaKey
- const ctrl_a = ctrl && event.code === "KeyA"
- const ctrl_c = ctrl && event.code === "KeyC"
-
- const start = event.code === "KeyS" || event.code === "F3"
- const pause = event.code === "KeyP" || event.code === "F4"
- const reannounce = event.code === "KeyR"
-
- const delete_ = event.code === "Delete"
- const delete_files = (shift || ctrl) && delete_
-
- // actions
- const action = start || pause || reannounce || delete_ || delete_files
- if (action && ids.length) {
- event.preventDefault();
- if (start)
- rpc.torrentStart(ids)
- else if (pause)
- rpc.torrentStop(ids)
- else if (reannounce)
- rpc.torrentReannounce(ids)
- else if (delete_files)
- torrentRemove(ids, true);
- else if (delete_)
- torrentRemove(ids, false)
- refreshTable({ fetchTorrents: true });
- return;
- }
+ const labels = {
+ supported: "Enabled",
+ numNodes: "Nodes",
+ numBuckets: "Buckets",
+ numTorrents: "Torrents",
+ numHeardFrom: "Heard from",
+ numFailedPeers: "Failed peers"
+ };
- // select all torrents
- if (ctrl_a) {
- event.preventDefault();
- util.sel.selectAll()
- refreshTable({ fetchTorrents: false });
- return;
- }
+ Object.keys(result).forEach(key => {
+ if (key in labels) {
+ const cell = document.createElement("li");
+ cell.textContent = `${labels[key]}: ${result[key]}`;
+ list.appendChild(cell);
+ }
+ });
- // copy magnet link
- if (ctrl_c) {
- event.preventDefault();
- if (ids.length)
- util.copy.magnets( util.sel.idsToObj(ids) );
- return;
- }
+ el_dht_stats_list.replaceChildren(list)
+ el_fs_dht_stats.style.display = "block";
+}
- // closing stuff
- if (esc) {
- if (!el.torrent_rcm.hidden) {
- // close rcm
- el.torrent_rcm.hidden = true;
- }
- else if (selected_torrents.length) {
- // deselecting torrents
- selected_torrents = []
- refreshTable({ fetchTorrents: false });
- }
- else if (selected_torrent) {
- // close details
- closeTorrentDetails();
- }
+el_torrents.addEventListener("click", async (event) => {
+ if (!jsLoaded())
return;
- }
- // select current torrent
- if (selected_torrent && space) {
- event.preventDefault();
- util.sel.toggleID(selected_torrent)
- refreshTable({ fetchTorrents: false });
- return;
+ const torrentActions = {
+ ".tr-start": async (id) => rpc.torrentStart([id]),
+ ".tr-pause": async (id) => rpc.torrentStop([id]),
+ ".tr-delete": async (id) => removeTorrent([id]),
+ ".tr-deletefiles": async (id) => removeTorrentAndFiles([id]),
+ ".tr-reannounce": async (id) => rpc.torrentReannounce([id]),
+
+ ".tr-start-all": async () => startAll(),
+ ".tr-pause-all": async () => stopAll(),
+ ".tr-reannounce-all": async () => rpc.torrentReannounce()
+ };
+
+ const button = event.target.closest(Object.keys(torrentActions).join(","));
+
+ if (button && el_torrents.contains(button)) {
+ const action = Object.keys(torrentActions).find(selector =>
+ button.matches(selector)
+ );
+
+ if (!action) return;
+
+ const id = Number(button.dataset.id || 0);
+ if (!Number.isSafeInteger(id)) return;
+
+ await torrentActions[action](id);
+ await refreshTable();
+ return;
}
- // moving selector
- if (selected_torrent && (up || down)){
- event.preventDefault();
-
- let idx = util.sel.objIdx(selected_torrent);
- if (up)
- idx -= 1
- if (down)
- idx += 1
-
- if (!torrents[idx])
- return;
-
- if (shift)
- util.sel.selectID(selected_torrent)
-
- selected_torrent = torrents[idx].id
-
- if (ctrl)
- util.sel.toggleID(selected_torrent)
- else if (shift)
- util.sel.selectID(selected_torrent)
+ const row = event.target.closest("tr");
+ if (!row || !el_torrents.contains(row)) return;
- refreshTable({ fetchTorrents: false });
+ const id = Number(row.dataset.id || 0);
+ if (!Number.isSafeInteger(id) || id <= 0) return;
- return;
- }
+ await openTorrentDetails([id]);
});
-async function refreshSessionStats() {
- if (!js()) return;
-
- const r = await rpc.sessionStats();
-
- if (!r?.arguments?.["current-stats"])
- return (el.fs_session_stats.style.display = "none")
-
- const list = util.list.session(r.arguments["current-stats"])
-
- el.session_stats_list.replaceChildren(list)
- el.fs_session_stats.style.display = "block";
-}
-
-async function refreshDhtStats() {
- if (!js()) return;
-
- const r = await rpc.dhtStats();
-
- if (!r?.arguments)
- return (el.fs_dht_stats.style.display = "none")
-
- const list = util.list.dht(r.arguments)
-
- el.dht_stats_list.replaceChildren(list)
- el.fs_dht_stats.style.display = "block";
-}
-
// refresh table with torrents
-async function refreshTable({
- closeDetails = false, fetchTorrents = true
-} = {}) {
+async function refreshTable() {
if (!window.rpc)
- return vis_exh('failed to load rpc.js', "")
+ return setError('failed to load rpc.js', "")
- if (!window.util)
- return vis_exh('failed to load utils.js', "")
+ if (!window.utils)
+ return setError('failed to load utils.js', "")
- if (fetchTorrents || !torrents.length) {
+ try {
const r = await rpc.torrentGet();
if (r?.result !== "success")
- return vis_exh('failed response', r)
+ return setError('failed response', r)
torrents = r.arguments.torrents;
- }
- if (!torrents.length) {
- el.torrents.replaceChildren();
- vis_exh();
- return;
- }
+ if (torrents.length === 0) {
+ el_torrents.replaceChildren();
+ clearError();
+ return;
+ }
- if (closeDetails)
- selected_torrent = 0
+ tr_table = utils.torrentTable(torrents)
- tr_table = util.table.torrents(torrents)
+ const fs = document.createElement("fieldset");
- const fs = document.createElement("fieldset");
+ const lable = document.createElement("legend");
+ lable.textContent = `Torrents: ${torrents.length}`
+ fs.append(lable, tr_table.table);
- const legend = document.createElement("legend");
- let legend_txt = `Torrents: ${torrents.length}`
- if (selected_torrents.length)
- legend_txt += `, Selected: ${selected_torrents.length}`
- legend.textContent = legend_txt
- fs.append(legend, tr_table.table);
-
- el.torrents.replaceChildren(fs);
-
- document.title =
- `β${util.fmt.bytes(tr_table.allDw)} β${util.fmt.bytes(tr_table.allUp)}`;
-
- selected_torrent
- ? openTorrentDetails(selected_torrent)
- : el.details.replaceChildren();
+ el_torrents.replaceChildren(fs);
+
+ document.title = `β${utils.formatBytes(tr_table.allDw)} β${utils.formatBytes(tr_table.allUp)}`;
- vis_exh();
- util.sel.clearIDs();
+ if (details_id !== null)
+ openTorrentDetails(details_id)
+
+ clearError()
+
+ } catch (err) {
+ setError('failed to update torrents table', err)
+ }
}
function openTorrentDetails(id) {
- if (!js()) return;
+ if (!jsLoaded())
+ return;
const torrent = torrents.find(t => t.id == id);
@@ -563,8 +277,10 @@
return;
- selected_torrent = torrent.id
+ details_id = torrent.id
+ // main frame
const fs = document.createElement("fieldset");
+ // main frame label
const content = document.createElement("div");
content.style.display = "flex";
@@ -573,5 +289,5 @@
const lable = document.createElement("span");
- lable.textContent = `#${torrent.id}: ${torrent.name}`;
+ lable.textContent = `#${torrent.id}: ${torrent.name} `;
const close_link = document.createElement("href");
@@ -590,14 +306,15 @@
fs.append(
- util.list.torrent(torrent),
- util.table.peers(torrent.peers),
- util.table.trackers(torrent.trackerStats),
- util.table.files(torrent.files)
+ utils.infoDetails(torrent),
+ utils.peerDetails(torrent.peers),
+ utils.trackerDetails(torrent.trackerStats),
+ utils.filesDetails(torrent.files)
);
- el.details.replaceChildren(fs);
+ el_details.replaceChildren(fs);
};
function closeTorrentDetails() {
- refreshTable({ closeDetails: true, fetchTorrents: false });
+ details_id = null;
+ el_details.replaceChildren();
};
\ No newline at end of file
diff -U2 -r /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/rpc.js /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/rpc.js
--- /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/rpc.js 2026-10-28 22:59:59.000000000 +0000
+++ /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/rpc.js 2026-10-28 22:33:51.000000000 +0000
@@ -45,14 +45,11 @@
},
- async torrentRemove(ids = [], withFiles = false) {
- if (!ids.length) // we don't want to remove all torrents
+ async removeTorrent(id, deletefiles = false) {
+ if (Array.isArray(id) && id.length === 0)
return;
- if (ids.length === 1)
- ids = ids[0]
-
return this.request("torrent-remove", {
- ids: ids,
- "delete-local-data": withFiles
+ ids: Array.isArray(id) ? id : [id],
+ "delete-local-data": deletefiles
});
},
diff -U2 -r /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/utils.js /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/utils.js
--- /var/lib/copr-rpmbuild/results/i2pd-git/upstream-unpacked/Source0/i2pd-openssl/contrib/torrents/web/utils.js 2026-10-28 22:59:59.000000000 +0000
+++ /var/lib/copr-rpmbuild/results/i2pd-git/srpm-unpacked/i2pd-openssl.tar.gz-extract/i2pd-openssl/contrib/torrents/web/utils.js 2026-10-28 22:33:51.000000000 +0000
@@ -1,766 +1,616 @@
-window.util = {
- sel: {
- redup(arr) { return [...new Set(arr)] },
-
- getObjects() { return (torrents || []).filter(item => (selected_torrents || []).includes(item.id)); },
- getAllIDs() { return torrents.map(item => item.id) },
- idsToObj(ids) { return torrents.filter(item => (ids || []).includes(item.id)) },
- objIdx(id) { return torrents.findIndex(item => item.id === id) },
-
- isEnabled() { return Boolean(selected_torrents.length) },
- isSelected(id) { return selected_torrents.includes(id) },
- isSomeRunning() { return this.getObjects().some(item => item.status > 0) },
- isSomeStopped() { return this.getObjects().some(item => item.status == 0) },
- allChecked() { return torrents.length === selected_torrents.length },
-
- deselectAll() { selected_torrents = [] },
- selectAll() { selected_torrents = this.getAllIDs() },
- toggleAll() { this.allChecked() ? this.deselectAll() : this.selectAll() },
- clearIDs() { selected_torrents = selected_torrents.filter(n => this.getAllIDs().includes(n)) },
+window.utils = {
+ formatBytes(bytesPerSec) {
+ if (!Number.isFinite(bytesPerSec) || bytesPerSec <= 0)
+ return "0 B/s";
+
+ const units = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
+ const i = Math.min(
+ Math.floor(Math.log(bytesPerSec) / Math.log(1024)),
+ units.length - 1
+ );
- toggleID(id) { this.isSelected(id) ? this.deselectID(id) : this.selectID(id) },
- toggleIDs(ids) { (ids || []).forEach(id => this.toggleID(id)) },
-
- deselectID(id) { selected_torrents = this.redup( selected_torrents.filter(n => n !== id)) },
- deselectIDs(ids) { (ids || []).forEach(id => this.deselectID(id)) },
-
- selectID(id) { selected_torrents = this.redup( [...selected_torrents, id] )},
- selectIDs(ids) { (ids || []).forEach(id => this.selectID(id)) }
+ return `${(bytesPerSec / 1024 ** i).toFixed(2)} ${units[i]}`;
},
- copy: {
- text(text) {
- try {
- if (navigator.clipboard?.writeText)
- navigator.clipboard.writeText(text);
- } catch {}
-
- const textarea = document.createElement("textarea");
- textarea.value = text;
- textarea.style.cssText = "position:fixed;opacity:0";
- document.body.appendChild(textarea);
- textarea.select();
-
- try {
- document.execCommand("copy");
- } finally {
- textarea.remove();
- }
- },
+ formatETA(torrent) {
+ const speed = torrent.rateDownload || 0;
+ const remaining = (torrent.totalSize || 0) * (1 - (torrent.percentDone || 0));
- magnets(torrents) {
- this.text(torrents.map(item => item.magnetLink).join("\n"));
- }
- },
-
- fmt: {
- bytes(bps) {
- if (!Number.isFinite(bps) || bps <= 0)
- return "0 B/s";
+ if (speed <= 0) return "";
- const units = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
- const i = Math.min(
- Math.floor(Math.log(bps) / Math.log(1024)),
- units.length - 1
- );
+ const seconds = remaining / speed;
- return `${(bps / 1024 ** i).toFixed(2)} ${units[i]}`;
- },
+ switch (true) {
+ case seconds < 60:
+ return `${Math.ceil(seconds)} sec`;
- eta(torrent) {
- const speed = torrent.rateDownload || 0;
- const remaining = (torrent.totalSize || 0) * (1 - (torrent.percentDone || 0));
+ case seconds < 3600:
+ return `${(seconds / 60).toFixed(1)} min`;
- if (speed <= 0) return "";
+ case seconds < 86400:
+ return `${(seconds / 3600).toFixed(1)} h`;
- const seconds = remaining / speed;
+ default:
+ return `${(seconds / 86400).toFixed(1)} days`;
+ }
+ },
- switch (true) {
- case seconds < 60:
- return `${Math.ceil(seconds)} sec`;
+ formatSize(bytes) {
+ if (!bytes) return "0 B";
- case seconds < 3600:
- return `${(seconds / 60).toFixed(1)} min`;
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
- case seconds < 86400:
- return `${(seconds / 3600).toFixed(1)} h`;
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
+ },
- default:
- return `${(seconds / 86400).toFixed(1)} days`;
+ infoDetails(torrent) {
+ if (!torrent)
+ return document.createElement("div");
+
+ const list = document.createElement("ul");
+
+ const keys = ["status", "hashString", "magnetLink", "pieceCount", "errorString"];
+
+ keys.forEach(key => {
+ const cell = document.createElement("li");
+ const value = torrent[key];
+
+ switch (key) {
+ case "hashString":
+ if (value)
+ cell.textContent = `Hash: ${value}`;
+ break;
+
+ case "magnetLink":
+ cell.textContent = `Magnet: `;
+
+ const link = document.createElement('a');
+ link.textContent = value.length > 60 ? value.slice(0, 60) + '...' : value;
+ link.href = value;
+ cell.appendChild(link);
+
+ break;
+
+ case "pieceCount":
+ if (value)
+ cell.textContent = `Pieces: ${value} x ${this.formatSize(torrent.pieceSize)}`;
+ break;
+
+ case "status":
+ const enumStatus = [
+ "Stopped",
+ "QueuedToVerifyLocalData",
+ "VerifyingLocalData",
+ "QueuedToDownload",
+ "Downloading",
+ "QueuedToSeed",
+ "Seeding"
+ ];
+
+ if (value >= 0 && value < enumStatus.length)
+ cell.textContent = `Status: ${enumStatus[value]}`;
+ break;
+
+ case "errorString":
+ if (value) {
+ cell.textContent = `Error: ${value || "None"}`;
+ cell.style.fontWeight = "bold";
+ cell.style.color = "#ff3333";
+ }
+ break;
}
- },
- size(bytes) {
- if (!bytes) return "0 B";
+ if (cell.textContent)
+ list.appendChild(cell);
+ });
- const units = ["B", "KB", "MB", "GB", "TB"];
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
-
- return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
- }
+ return list
},
- list: {
- session(session_stats) {
- const list = document.createElement("div");
-
- const fields = {
- downloadedBytes: "Downloaded",
- uploadedBytes: "Uploaded"
- };
-
- Object.keys(session_stats).forEach(key => {
- if (key in fields) {
- const cell = document.createElement("li");
- cell.textContent = `${fields[key]}: ${util.fmt.size(session_stats[key])}`;
- list.appendChild(cell);
- }
- });
-
- return list
- },
+ filesDetails(files) {
+ if (!files?.length)
+ return document.createElement("div");
+
+ // files frame
+ const fs_files = document.createElement("fieldset");
+
+ // files frame label
+ const fs_files_lable = document.createElement("legend");
+ fs_files_lable.textContent = `Files: ${files.length}`
+ fs_files.appendChild(fs_files_lable);
+
+ // files fields
+ const files_table_cols = ["name", "progress", "size", "have", "left"];
+
+ // files table
+ const files_table = document.createElement("table");
+ files_table.className = "compact-table";
+
+ // files table header
+ files_table_cols.forEach((item) => {
+ const th = document.createElement("th");
+ th.textContent = item;
+ th.style.whiteSpace = 'nowrap'
+ files_table.appendChild(th);
+ });
- dht(dht_stats) {
- const list = document.createElement("div");
-
- const fields = {
- supported: "Enabled",
- numNodes: "Nodes",
- numBuckets: "Buckets",
- numTorrents: "Torrents",
- numHeardFrom: "Heard from",
- numFailedPeers: "Failed peers"
- };
-
- Object.keys(dht_stats).forEach(key => {
- if (key in fields) {
- const cell = document.createElement("li");
- cell.textContent = `${fields[key]}: ${dht_stats[key]}`;
- list.appendChild(cell);
- }
- });
-
- return list
- },
+ // files table data
+ files.forEach((file, index) => {
+ const tr = document.createElement("tr");
- torrent(torrent) {
- if (!torrent)
- return document.createElement("div");
-
- const keys = ["status", "hashString", "magnetLink", "pieceCount", "errorString"];
- const list = document.createElement("ul");
-
- keys.forEach(key => {
- const cell = document.createElement("li");
- const value = torrent[key];
+ files_table_cols.forEach(key => {
+ const cell = document.createElement("td");
+ const value = file[key];
switch (key) {
- case "hashString":
- if (value)
- cell.textContent = `Hash: ${value}`;
+ case "name":
+ cell.textContent = value;
break;
+ case "progress":
+ cell.className = "progress-cell";
- case "magnetLink":
- cell.textContent = `Magnet: `;
-
- const link = document.createElement('a');
- link.textContent = value.length > 60 ? value.slice(0, 60) + '...' : value;
- link.href = value;
+ const progress = file.length > 0 ?
+ Math.min(
+ 1,
+ Math.max(
+ 0,
+ file.bytesCompleted / file.length
+ )
+ ) : 0;
- link.addEventListener("click", event => {
- event.preventDefault();
- util.copy.magnets([torrent])
- });
- cell.appendChild(link);
+ const percent = +(progress * 100).toFixed(1);
- break;
+ const progressFill = document.createElement("div");
+ progressFill.className = "progress-fill";
+ progressFill.style.width = `${percent}%`;
+ progressFill.style.backgroundColor = progress === 1 ? "#00FF5A" : "#4DAFFF";
- case "pieceCount":
- if (value)
- cell.textContent = `Pieces: ${value} x ${util.fmt.size(torrent.pieceSize)}`;
- break;
+ const progressBar = document.createElement("div");
+ progressBar.className = "progress-bar";
+ progressBar.appendChild(progressFill);
- case "status":
- const enumStatus = [
- "Stopped",
- "QueuedToVerifyLocalData",
- "VerifyingLocalData",
- "QueuedToDownload",
- "Downloading",
- "QueuedToSeed",
- "Seeding"
- ];
+ const progressText = document.createElement("div");
+ progressText.className = "progress-text";
+ progressText.textContent = `${percent}%`;
- if (value >= 0 && value < enumStatus.length)
- cell.textContent = `Status: ${enumStatus[value]}`;
+ cell.append(progressBar, progressText);
break;
-
- case "errorString":
- if (value) {
- cell.textContent = `Error: ${value || "None"}`;
- cell.style.fontWeight = "bold";
- cell.style.color = "#ff3333";
- }
+ case "size":
+ cell.textContent = this.formatSize(file.length);
+ break;
+ case "have":
+ cell.textContent = file.bytesCompleted ? this.formatSize(file.bytesCompleted) : "";
+ break;
+ case "left":
+ cell.textContent = (file.length - file.bytesCompleted) ? this.formatSize(file.length - file.bytesCompleted) : "";
break;
}
- if (cell.textContent)
- list.appendChild(cell);
- });
-
- return list
- }
- },
-
- table: {
- torrents(torrents) {
- if (!torrents?.length)
- return {
- "table": document.createElement("div"),
- "allDw": 0,
- "allUp": 0,
- "allSize": 0
- }
-
- let allDw = allUp = allSize = 0;
-
- const table_cols = ["_select_", "id", "name", "progress", "S / L", "speedDw", "speedUp", "size", "have", "eta"];
-
- const table = document.createElement("table");
- table.className = "compact-table";
-
- table_cols.forEach((item) => {
- const th = document.createElement("th");
-
- if (item === "_select_")
- {
- if (!util.sel.isEnabled())
- return;
-
- const checkbox = document.createElement("input");
- checkbox.type = "checkbox";
- th.style.textAlign = "center";
-
- if (util.sel.allChecked())
- checkbox.checked = true;
-
- checkbox.addEventListener("change", () => {
- util.sel.toggleAll();
- refreshTable({ fetchTorrents: false });
- });
-
- th.append(checkbox);
- table.appendChild(th);
-
- return;
- }
-
- th.textContent = item;
- th.style.whiteSpace = 'nowrap'
+ if (key !== "name")
+ cell.style.whiteSpace = 'nowrap'
- table.appendChild(th);
+ tr.appendChild(cell);
});
- torrents.forEach((torrent, index) => {
- const tr = document.createElement("tr");
- const is_magnet = torrent.metadataPercentComplete < 1
-
- table_cols.forEach(key => {
- if (key === "_select_" && !util.sel.isEnabled())
- return;
-
- const cell = document.createElement("td");
-
- switch (key) {
- case "_select_":
- const checkbox = document.createElement("input");
- checkbox.type = "checkbox";
-
- if (util.sel.isSelected(torrent.id))
- checkbox.checked = true;
-
- checkbox.addEventListener("change", () => {
- util.sel.toggleID(torrent.id);
- refreshTable({ fetchTorrents: false });
- });
-
- cell.style.textAlign = "center";
- cell.append(checkbox);
- break;
-
- case "id":
- cell.textContent = torrent.id || " ";
- break;
-
- case "name":
- cell.textContent = torrent.name || " ";
- if (torrent.errorString) {
- cell.style.fontWeight = "bold";
- cell.style.color = "#ff3333";
- }
- break;
-
- case "progress": {
- cell.className = "progress-cell";
-
- const percent = Math.max(
- 0,
- Math.min(
- 100,
- (torrent.percentDone * 100).toFixed(1)
- )
- )
-
- let pr_color = "#E5E7EB"
- let pr_lable = is_magnet ? 'π§²' : `${percent}%`;
- switch (torrent.status) {
- case 4:
- pr_color = "#44abff"; // downloading
- if (!torrent.rateDownload && !is_magnet) {
- pr_color = "#0072cf";
- pr_lable += ', stalled'
- }
- break;
- case 6:
- pr_color = "#00FF5A"; // seeding
- break;
- case 0:
- pr_color = "#009133"; // paused + done
- if (torrent.percentDone < 1) {
- pr_color = "#ffd000"; // paused
- pr_lable += ', paused'
- }
- break;
- }
-
- const progressBar = document.createElement("div");
- progressBar.className = "progress-bar";
-
- const progressFill = document.createElement("div");
- progressFill.className = "progress-fill";
- progressFill.style.width = `${percent}%`;
- progressFill.style.backgroundColor = pr_color;
- progressBar.appendChild(progressFill);
-
- const progressText = document.createElement("div");
- progressText.className = "progress-text";
- progressText.textContent = pr_lable
-
- cell.append(progressBar, progressText);
- break;
- }
-
- case "S / L":
- let seeders = leechers = 0;
-
- torrent.peers.forEach(peer => {
- if (peer.progress !== 0)
- peer.progress === 1 ? seeders++ : leechers++
- });
-
- if (torrent.status === 6) // seeding
- cell.textContent = leechers || ""
- else if (seeders || leechers) // leeching
- cell.textContent = `${seeders} / ${leechers}`;
-
- break;
-
- case "speedDw":
- cell.textContent = torrent.rateDownload
- ? util.fmt.bytes(torrent.rateDownload)
- : "";
-
- allDw += torrent.rateDownload;
- break;
-
- case "speedUp":
- cell.textContent = torrent.rateUpload
- ? util.fmt.bytes(torrent.rateUpload)
- : "";
-
- allUp += torrent.rateUpload;
- break;
-
- case "size":
- cell.textContent = torrent.totalSize
- ? util.fmt.size(torrent.totalSize)
- : "";
-
- allSize += torrent.totalSize
- break;
-
- case "have":
- cell.textContent = (torrent.haveValid === 0 || torrent.haveValid === torrent.totalSize)
- ? ""
- : util.fmt.size(torrent.haveValid);
-
- break;
-
- case "eta":
- cell.textContent = util.fmt.eta(torrent);
- break;
- }
-
- if (key !== "name")
- cell.style.whiteSpace = 'nowrap'
-
- tr.appendChild(cell);
- });
+ if (index % 2)
+ tr.style.backgroundColor = "#ebebeb";
- // zebra
- if (index % 2)
- tr.style.backgroundColor = "#ebebeb";
+ files_table.appendChild(tr);
+ });
- const is_selector = selected_torrent === torrent.id;
- const is_selected = util.sel.isSelected(torrent.id);
+ fs_files.appendChild(files_table);
- if (is_selector)
- tr.style.backgroundColor = "#ff9900ce";
-
- if (is_selected) {
- tr.style.backgroundColor = "#0000a1";
- tr.style.color = "#fff";
- }
-
- if (is_selector && is_selected)
- tr.style.backgroundColor = "#8050d0";
-
- tr.dataset.id = torrent.id
- tr.classList.add("torrent-row");
+ return fs_files
+ },
- table.appendChild(tr);
- });
+ trackerDetails(trackerStats) {
+ if (!trackerStats?.length)
+ return document.createElement("div");
+
+ const fs = document.createElement("fieldset");
+
+ // trackers frame label
+ const fs_lable = document.createElement("legend");
+ fs_lable.textContent = `Trackers: ${trackerStats.length}`
+ fs.appendChild(fs_lable);
+
+ // trackers fields
+ const table_cols = [
+ "id", "announce",
+ "peers", "seeders", "leechers",
+ "lastAnnounceResult", "lastAnnounceTime", "nextAnnounceTime"
+ ];
+
+ // trackers table
+ const table = document.createElement("table");
+ table.className = "compact-table";
+
+ // trackers table header
+ table_cols.forEach((item) => {
+ const th = document.createElement("th");
+ th.textContent = item;
+ th.style.whiteSpace = 'nowrap'
+ table.appendChild(th);
+ });
- // bottom row
+ trackerStats.forEach((tracker, index) => {
const tr = document.createElement("tr");
- table_cols.forEach(key => {
- if (key === "_select_" && !util.sel.isEnabled())
- return;
+ table_cols.forEach(key => {
const cell = document.createElement("td");
+ const value = tracker[key];
switch (key) {
- case "speedDw":
- cell.textContent = allDw
- ? util.fmt.bytes(allDw)
- : "";
-
+ case "announce":
+ case "lastAnnounceResult":
+ cell.textContent = value || "";
break;
-
- case "speedUp":
- cell.textContent = allUp
- ? util.fmt.bytes(allUp)
- : "";
-
+ case "id":
+ cell.textContent = value;
break;
-
- case "size":
- cell.textContent = allSize
- ? util.fmt.size(allSize)
- : "";
-
+ case "seeders":
+ cell.textContent = tracker.seederCount || 0;
+ break;
+ case "leechers":
+ cell.textContent = tracker.leecherCount || 0;
+ break;
+ case "peers":
+ cell.textContent = tracker.lastAnnouncePeerCount || 0;
+ break;
+ case "lastAnnounceTime":
+ case "nextAnnounceTime":
+ cell.textContent = new Date(value * 1000).toLocaleString();
break;
}
-
- cell.style.fontWeight = "bold";
- cell.style.whiteSpace = 'nowrap'
+
+ if (key !== "announce")
+ cell.style.whiteSpace = 'nowrap'
+
tr.appendChild(cell);
});
+ if (index % 2)
+ tr.style.backgroundColor = "#ebebeb";
+
table.appendChild(tr);
+ });
- return {
- "table": table,
- "allDw": allDw,
- "allUp": allUp,
- "allSize": allSize
- }
- },
+ fs.appendChild(table);
- files(files) {
- if (!files?.length)
- return document.createElement("div");
-
- // files frame
- const fs = document.createElement("fieldset");
-
- // files frame label
- const fs_lable = document.createElement("legend");
- fs_lable.textContent = `Files: ${files.length}`
- fs.appendChild(fs_lable);
-
- // files fields
- const table_cols = ["name", "progress", "size", "have", "left"];
-
- // files table
- const table = document.createElement("table");
- table.className = "compact-table";
-
- // files table header
- table_cols.forEach((item) => {
- const th = document.createElement("th");
- th.textContent = item;
- th.style.whiteSpace = 'nowrap'
- table.appendChild(th);
- });
-
- // files table data
- files.forEach((file, index) => {
- const tr = document.createElement("tr");
-
- table_cols.forEach(key => {
- const cell = document.createElement("td");
- const value = file[key];
-
- switch (key) {
- case "name":
- cell.textContent = value;
- break;
- case "progress":
- cell.className = "progress-cell";
-
- const progress = file.length > 0 ?
- Math.min(
- 1,
- Math.max(
- 0,
- file.bytesCompleted / file.length
- )
- ) : 0;
-
- const percent = +(progress * 100).toFixed(1);
-
- const progressFill = document.createElement("div");
- progressFill.className = "progress-fill";
- progressFill.style.width = `${percent}%`;
- progressFill.style.backgroundColor = progress === 1 ? "#00FF5A" : "#4DAFFF";
-
- const progressBar = document.createElement("div");
- progressBar.className = "progress-bar";
- progressBar.appendChild(progressFill);
-
- const progressText = document.createElement("div");
- progressText.className = "progress-text";
- progressText.textContent = `${percent}%`;
-
- cell.append(progressBar, progressText);
- break;
-
- case "size":
- cell.textContent = util.fmt.size(file.length);
- break;
-
- case "have":
- cell.textContent = file.bytesCompleted
- ? util.fmt.size(file.bytesCompleted)
- : "";
- break;
-
- case "left":
- cell.textContent = (file.length - file.bytesCompleted)
- ? util.fmt.size(file.length - file.bytesCompleted)
- : "";
- break;
- }
-
- if (key !== "name")
- cell.style.whiteSpace = 'nowrap'
+ return fs
+ },
- tr.appendChild(cell);
- });
+ peerDetails(peers) {
+ if (!peers?.length)
+ return document.createElement("div");
+
+ // peers frame
+ const fs = document.createElement("fieldset");
+
+ // peers frame label
+ const fs_lable = document.createElement("legend");
+ fs_lable.textContent = `Peers: ${peers.length}`
+ fs.appendChild(fs_lable);
+
+ // peer fields
+ const table_cols = [
+ "address", "client", "flags",
+ "progress", "speedDw", "speedUp"
+ ];
+
+ // peer table
+ const table = document.createElement("table");
+ table.className = "compact-table";
+
+ // peer table header
+ table_cols.forEach((item) => {
+ const th = document.createElement("th");
+ th.textContent = item;
+ table.appendChild(th);
+ });
- // zebra
- if (index % 2)
- tr.style.backgroundColor = "#ebebeb";
+ // peer table data
+ peers.forEach((peer, index) => {
+ const tr = document.createElement("tr");
- table.appendChild(tr);
- });
+ table_cols.forEach(key => {
+ const cell = document.createElement("td");
+ const value = peer[key];
- fs.appendChild(table);
+ switch (key) {
+ case "address":
+ cell.textContent = value;
+ break;
- return fs
- },
+ case "client":
+ cell.textContent = peer.clientName || "";
+ break;
- trackers(trackerStats) {
- if (!trackerStats?.length)
- return document.createElement("div");
-
- const fs = document.createElement("fieldset");
-
- // trackers frame label
- const fs_lable = document.createElement("legend");
- fs_lable.textContent = `Trackers: ${trackerStats.length}`
- fs.appendChild(fs_lable);
-
- // trackers fields
- const table_cols = [
- "id", "announce",
- "peers", "seeders", "leechers",
- "lastAnnounceResult", "lastAnnounceTime", "nextAnnounceTime"
- ];
-
- // trackers table
- const table = document.createElement("table");
- table.className = "compact-table";
-
- // trackers table header
- table_cols.forEach((item) => {
- const th = document.createElement("th");
- th.textContent = item;
- th.style.whiteSpace = 'nowrap'
- table.appendChild(th);
- });
+ case "flags":
+ cell.textContent = peer.flagStr || "";
+ break;
- trackerStats.forEach((tracker, index) => {
- const tr = document.createElement("tr");
+ case "progress":
+ cell.className = "progress-cell";
- table_cols.forEach(key => {
- const cell = document.createElement("td");
- const value = tracker[key];
-
- switch (key) {
- case "announce":
- case "lastAnnounceResult":
- cell.textContent = value || "";
- break;
- case "id":
- cell.textContent = value;
- break;
- case "seeders":
- cell.textContent = tracker.seederCount || 0;
- break;
- case "leechers":
- cell.textContent = tracker.leecherCount || 0;
- break;
- case "peers":
- cell.textContent = tracker.lastAnnouncePeerCount || 0;
- break;
- case "lastAnnounceTime":
- case "nextAnnounceTime":
- cell.textContent = new Date(value * 1000).toLocaleString();
- break;
- }
+ const percent = Math.max(
+ 0,
+ Math.min(
+ 100,
+ (peer.progress * 100).toFixed(1)
+ )
+ )
- if (key !== "announce")
- cell.style.whiteSpace = 'nowrap'
+ const progressBar = document.createElement("div");
+ progressBar.className = "progress-bar";
- tr.appendChild(cell);
- });
+ const progressFill = document.createElement("div");
+ progressFill.className = "progress-fill";
+ progressFill.style.width = `${percent}%`;
+ progressFill.style.backgroundColor = peer.progress >= 1 ? "#00FF5A" : "#4DAFFF";
+ progressBar.appendChild(progressFill);
+
+ const progressText = document.createElement("div");
+ progressText.className = "progress-text";
+ progressText.textContent = `${percent}%`;
- // zebra
- if (index % 2)
- tr.style.backgroundColor = "#ebebeb";
-
- table.appendChild(tr);
- });
+ cell.append(progressBar, progressText);
+ break;
- fs.appendChild(table);
+ case "speedDw":
+ cell.textContent = peer.rateToClient ? this.formatBytes(peer.rateToClient) : "";
+ break;
- return fs
- },
+ case "speedUp":
+ cell.textContent = peer.rateToPeer ? this.formatBytes(peer.rateToPeer) : "";
+ break;
+ }
- peers(peers) {
- if (!peers?.length)
- return document.createElement("div");
-
- // peers frame
- const fs = document.createElement("fieldset");
-
- // peers frame label
- const fs_lable = document.createElement("legend");
- fs_lable.textContent = `Peers: ${peers.length}`
- fs.appendChild(fs_lable);
-
- // peer fields
- const table_cols = [
- "address", "client", "flags",
- "progress", "speedDw", "speedUp"
- ];
-
- // peer table
- const table = document.createElement("table");
- table.className = "compact-table";
-
- // peer table header
- table_cols.forEach((item) => {
- const th = document.createElement("th");
- th.textContent = item;
- table.appendChild(th);
+ tr.appendChild(cell);
});
- // peer table data
- peers.forEach((peer, index) => {
- const tr = document.createElement("tr");
-
- table_cols.forEach(key => {
- const cell = document.createElement("td");
- const value = peer[key];
-
- switch (key) {
- case "address":
- cell.textContent = value;
- break;
-
- case "client":
- cell.textContent = peer.clientName || "";
- break;
-
- case "flags":
- cell.textContent = peer.flagStr || "";
- break;
-
- case "progress":
- cell.className = "progress-cell";
-
- const percent = Math.max(
- 0,
- Math.min(
- 100,
- (peer.progress * 100).toFixed(1)
- )
- )
-
- const progressBar = document.createElement("div");
- progressBar.className = "progress-bar";
-
- const progressFill = document.createElement("div");
- progressFill.className = "progress-fill";
- progressFill.style.width = `${percent}%`;
- progressFill.style.backgroundColor = peer.progress >= 1 ? "#00FF5A" : "#4DAFFF";
- progressBar.appendChild(progressFill);
-
- const progressText = document.createElement("div");
- progressText.className = "progress-text";
- progressText.textContent = `${percent}%`;
-
- cell.append(progressBar, progressText);
- break;
-
- case "speedDw":
- cell.textContent = peer.rateToClient
- ? util.fmt.bytes(peer.rateToClient)
- : "";
- break;
-
- case "speedUp":
- cell.textContent = peer.rateToPeer
- ? util.fmt.bytes(peer.rateToPeer)
- : "";
- break;
- }
-
- tr.appendChild(cell);
- });
+ if (index % 2)
+ tr.style.backgroundColor = "#ebebeb";
- // zebra
- if (index % 2)
- tr.style.backgroundColor = "#ebebeb";
+ table.appendChild(tr);
+ });
- table.appendChild(tr);
- });
+ fs.appendChild(table);
- fs.appendChild(table);
+ return fs
+ },
- return fs
+ torrentTable(torrents) {
+ if (!torrents?.length)
+ return {
+ "table": document.createElement("div"),
+ "allDw": 0,
+ "allUp": 0,
+ "allSize": 0
+ }
+
+ let allDw = allUp = allSize = 0;
+
+ const buttons = [
+ { className: "tr-start", text: "βΆοΈ", title: "Start" },
+ { className: "tr-pause", text: "βΈοΈ", title: "Pause" },
+ { className: "tr-reannounce", text: "π", title: "Reannounce" },
+ { className: "tr-delete", text: "β", title: "Delete torrent"},
+ { className: "tr-deletefiles", text: "ποΈ", title: "Delete torrent and files"}
+ ];
+
+ const buttonsAll = [
+ { className: "tr-start-all", text: "βΆοΈ", title: "Start all" },
+ { className: "tr-pause-all", text: "βΈοΈ", title: "Pause all" },
+ { className: "tr-reannounce-all", text: "π", title: "Reannounce all" }
+ ];
+
+ const table_cols = ["id", "name", "progress", "S / L", "speedDw", "speedUp", "size", "have", "eta", "actions"];
+
+ const table = document.createElement("table");
+ table.className = "compact-table";
+
+ table_cols.forEach((item) => {
+ const th = document.createElement("th");
+ th.textContent = item;
+ th.style.whiteSpace = 'nowrap'
+ table.appendChild(th);
+ });
+
+ torrents.forEach((torrent, index) => {
+ const tr = document.createElement("tr");
+ const is_magnet = torrent.metadataPercentComplete < 1
+
+ table_cols.forEach(key => {
+ const cell = document.createElement("td");
+
+ switch (key) {
+ case "id":
+ cell.textContent = torrent.id || " ";
+ break;
+
+ case "name":
+ cell.textContent = torrent.name || " ";
+ if (torrent.errorString) {
+ cell.style.fontWeight = "bold";
+ cell.style.color = "#ff3333";
+ }
+ break;
+
+ case "progress": {
+ cell.className = "progress-cell";
+
+ const percent = Math.max(
+ 0,
+ Math.min(
+ 100,
+ (torrent.percentDone * 100).toFixed(1)
+ )
+ )
+
+ pr_color = "#E5E7EB"
+ pr_lable = is_magnet ? 'π§²' : `${percent}%`;
+ switch (torrent.status) {
+ case 4:
+ pr_color = "#44abff"; // downloading
+ if (!torrent.rateDownload && !is_magnet) {
+ pr_color = "#0072cf"; // stalled
+ pr_lable += ', stalled'
+ }
+ break;
+ case 6:
+ pr_color = "#00FF5A"; // seeding
+ break;
+ case 0:
+ pr_color = "#009133"; // paused + done
+ if (torrent.percentDone < 1) {
+ pr_color = "#ffd000"; // paused
+ pr_lable += ', paused'
+ }
+ break;
+ }
+
+ const progressBar = document.createElement("div");
+ progressBar.className = "progress-bar";
+
+ const progressFill = document.createElement("div");
+ progressFill.className = "progress-fill";
+ progressFill.style.width = `${percent}%`;
+ progressFill.style.backgroundColor = pr_color;
+ progressBar.appendChild(progressFill);
+
+ const progressText = document.createElement("div");
+ progressText.className = "progress-text";
+ progressText.textContent = pr_lable
+
+ cell.append(progressBar, progressText);
+ break;
+ }
+
+ case "S / L":
+ let seeders = leechers = 0;
+
+ torrent.peers.forEach(peer => {
+ if (peer.progress !== 0)
+ peer.progress === 1 ? seeders++ : leechers++
+ });
+
+ if (torrent.status === 6) // seeding
+ cell.textContent = leechers || ""
+ else if (seeders || leechers) // leeching
+ cell.textContent = `${seeders} / ${leechers}`;
+
+ break;
+
+ case "speedDw":
+ cell.textContent = torrent.rateDownload ? utils.formatBytes(torrent.rateDownload) : "";
+ allDw += torrent.rateDownload;
+ break;
+
+ case "speedUp":
+ cell.textContent = torrent.rateUpload ? utils.formatBytes(torrent.rateUpload) : "";
+ allUp += torrent.rateUpload;
+ break;
+
+ case "size":
+ cell.textContent = torrent.totalSize ? utils.formatSize(torrent.totalSize) : "";
+ allSize += torrent.totalSize
+ break;
+
+ case "have":
+ cell.textContent = (torrent.haveValid === 0 || torrent.haveValid === torrent.totalSize) ? "" : utils.formatSize(torrent.haveValid);
+ break;
+
+ case "eta":
+ cell.textContent = utils.formatETA(torrent);
+ break;
+
+ case "actions":
+ cell.className = "buttons-cell";
+
+ const buttonRow = document.createElement("div");
+ buttonRow.className = "button-row";
+
+ buttons.forEach(({className, text, title}) => {
+ const button = document.createElement("button");
+ button.className = className;
+ button.textContent = text;
+ button.dataset.id = torrent.id;
+ button.title = title;
+
+ buttonRow.appendChild(button);
+ });
+
+ cell.appendChild(buttonRow);
+ break;
+ }
+
+ if (key !== "name")
+ cell.style.whiteSpace = 'nowrap'
+
+ tr.appendChild(cell);
+ });
+
+ if (index % 2)
+ tr.style.backgroundColor = "#ebebeb";
+
+ tr.dataset.id = torrent.id
+ tr.classList.add("torrent-row");
+
+ table.appendChild(tr);
+ });
+
+ // bottom row
+ const tr = document.createElement("tr");
+ table_cols.forEach(key => {
+ const cell = document.createElement("td");
+
+ switch (key) {
+ case "speedDw":
+ cell.textContent = allDw ? utils.formatBytes(allDw) : "";
+ cell.style.fontWeight = "bold";
+ break;
+
+ case "speedUp":
+ cell.textContent = allUp ? utils.formatBytes(allUp) : "";
+ cell.style.fontWeight = "bold";
+ break;
+
+ case "size":
+ cell.textContent = allSize ? utils.formatSize(allSize) : "";
+ cell.style.fontWeight = "bold";
+ break;
+
+ case "actions":
+ cell.className = "buttons-cell";
+
+ const buttonRow = document.createElement("div");
+ buttonRow.className = "button-row";
+
+ buttonsAll.forEach(({className, text, title}) => {
+ const button = document.createElement("button");
+ button.className = className;
+ button.textContent = text;
+ button.title = title;
+
+ buttonRow.appendChild(button);
+ });
+
+ cell.appendChild(buttonRow);
+ break;
+ }
+
+ cell.style.whiteSpace = 'nowrap'
+ tr.appendChild(cell);
+ });
+
+ table.appendChild(tr);
+
+ return {
+ "table": table,
+ "allDw": allDw,
+ "allUp": allUp,
+ "allSize": allSize
}
}