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; -} - -

failed to load main.js

-

+

@@ -152,5 +127,5 @@ Actions
- + 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 } }