2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2009 Christophe Dumez <chris@qbittorrent.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
31 window.qBittorrent ??= {};
32 window.qBittorrent.PropFiles ??= (() => {
33 const exports = () => {
35 normalizePriority: normalizePriority,
36 isDownloadCheckboxExists: isDownloadCheckboxExists,
37 createDownloadCheckbox: createDownloadCheckbox,
38 updateDownloadCheckbox: updateDownloadCheckbox,
39 isPriorityComboExists: isPriorityComboExists,
40 createPriorityCombo: createPriorityCombo,
41 updatePriorityCombo: updatePriorityCombo,
42 updateData: updateData,
43 collapseIconClicked: collapseIconClicked,
44 expandFolder: expandFolder,
45 collapseFolder: collapseFolder
49 const torrentFilesTable = new window.qBittorrent.DynamicTable.TorrentFilesTable();
50 const FilePriority = window.qBittorrent.FileTree.FilePriority;
51 const TriState = window.qBittorrent.FileTree.TriState;
53 let current_hash = "";
55 const normalizePriority = function(priority) {
57 case FilePriority.Ignored:
58 case FilePriority.Normal:
59 case FilePriority.High:
60 case FilePriority.Maximum:
61 case FilePriority.Mixed:
64 return FilePriority.Normal;
68 const getAllChildren = function(id, fileId) {
69 const node = torrentFilesTable.getNode(id);
80 const getChildFiles = function(node) {
82 node.children.each((child) => {
87 rowIds.push(node.data.rowId);
88 fileIds.push(node.data.fileId);
92 node.children.each((child) => {
102 const fileCheckboxClicked = function(e) {
105 const checkbox = e.target;
106 const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored;
107 const id = checkbox.getAttribute("data-id");
108 const fileId = checkbox.getAttribute("data-file-id");
110 const rows = getAllChildren(id, fileId);
112 setFilePriority(rows.rowIds, rows.fileIds, priority);
113 updateGlobalCheckbox();
116 const fileComboboxChanged = function(e) {
117 const combobox = e.target;
118 const priority = combobox.value;
119 const id = combobox.getAttribute("data-id");
120 const fileId = combobox.getAttribute("data-file-id");
122 const rows = getAllChildren(id, fileId);
124 setFilePriority(rows.rowIds, rows.fileIds, priority);
125 updateGlobalCheckbox();
128 const isDownloadCheckboxExists = function(id) {
129 return ($("cbPrio" + id) !== null);
132 const createDownloadCheckbox = function(id, fileId, checked) {
133 const checkbox = new Element("input");
134 checkbox.type = "checkbox";
135 checkbox.id = "cbPrio" + id;
136 checkbox.setAttribute("data-id", id);
137 checkbox.setAttribute("data-file-id", fileId);
138 checkbox.className = "DownloadedCB";
139 checkbox.addEventListener("click", fileCheckboxClicked);
141 updateCheckbox(checkbox, checked);
145 const updateDownloadCheckbox = function(id, checked) {
146 const checkbox = $("cbPrio" + id);
147 updateCheckbox(checkbox, checked);
150 const updateCheckbox = function(checkbox, checked) {
152 case TriState.Checked:
153 setCheckboxChecked(checkbox);
155 case TriState.Unchecked:
156 setCheckboxUnchecked(checkbox);
158 case TriState.Partial:
159 setCheckboxPartial(checkbox);
164 const isPriorityComboExists = function(id) {
165 return ($("comboPrio" + id) !== null);
168 const createPriorityCombo = (id, fileId, selectedPriority) => {
169 const createOption = (priority, isSelected, text) => {
170 const option = document.createElement("option");
171 option.value = priority.toString();
172 option.selected = isSelected;
173 option.textContent = text;
177 const select = document.createElement("select");
178 select.id = "comboPrio" + id;
179 select.setAttribute("data-id", id);
180 select.setAttribute("data-file-id", fileId);
181 select.addClass("combo_priority");
182 select.addEventListener("change", fileComboboxChanged);
184 select.appendChild(createOption(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]"));
185 select.appendChild(createOption(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]"));
186 select.appendChild(createOption(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]"));
187 select.appendChild(createOption(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]"));
189 // "Mixed" priority is for display only; it shouldn't be selectable
190 const mixedPriorityOption = createOption(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
191 mixedPriorityOption.disabled = true;
192 select.appendChild(mixedPriorityOption);
197 const updatePriorityCombo = function(id, selectedPriority) {
198 const combobox = $("comboPrio" + id);
199 if (parseInt(combobox.value, 10) !== selectedPriority)
200 selectComboboxPriority(combobox, selectedPriority);
203 const selectComboboxPriority = function(combobox, priority) {
204 const options = combobox.options;
205 for (let i = 0; i < options.length; ++i) {
206 const option = options[i];
207 if (parseInt(option.value, 10) === priority)
208 option.selected = true;
210 option.selected = false;
213 combobox.value = priority;
216 const switchCheckboxState = function(e) {
221 let priority = FilePriority.Ignored;
222 const checkbox = $("tristate_cb");
224 if (checkbox.state === "checked") {
225 setCheckboxUnchecked(checkbox);
226 // set file priority for all checked to Ignored
227 torrentFilesTable.getFilteredAndSortedRows().forEach((row) => {
228 const rowId = row.rowId;
229 const fileId = row.full_data.fileId;
230 const isChecked = (row.full_data.checked === TriState.Checked);
231 const isFolder = (fileId === -1);
232 if (!isFolder && isChecked) {
234 fileIds.push(fileId);
239 setCheckboxChecked(checkbox);
240 priority = FilePriority.Normal;
241 // set file priority for all unchecked to Normal
242 torrentFilesTable.getFilteredAndSortedRows().forEach((row) => {
243 const rowId = row.rowId;
244 const fileId = row.full_data.fileId;
245 const isUnchecked = (row.full_data.checked === TriState.Unchecked);
246 const isFolder = (fileId === -1);
247 if (!isFolder && isUnchecked) {
249 fileIds.push(fileId);
254 if (rowIds.length > 0)
255 setFilePriority(rowIds, fileIds, priority);
258 const updateGlobalCheckbox = function() {
259 const checkbox = $("tristate_cb");
260 if (isAllCheckboxesChecked())
261 setCheckboxChecked(checkbox);
262 else if (isAllCheckboxesUnchecked())
263 setCheckboxUnchecked(checkbox);
265 setCheckboxPartial(checkbox);
268 const setCheckboxChecked = function(checkbox) {
269 checkbox.state = "checked";
270 checkbox.indeterminate = false;
271 checkbox.checked = true;
274 const setCheckboxUnchecked = function(checkbox) {
275 checkbox.state = "unchecked";
276 checkbox.indeterminate = false;
277 checkbox.checked = false;
280 const setCheckboxPartial = function(checkbox) {
281 checkbox.state = "partial";
282 checkbox.indeterminate = true;
285 const isAllCheckboxesChecked = function() {
286 const checkboxes = $$("input.DownloadedCB");
287 for (let i = 0; i < checkboxes.length; ++i) {
288 if (!checkboxes[i].checked)
294 const isAllCheckboxesUnchecked = function() {
295 const checkboxes = $$("input.DownloadedCB");
296 for (let i = 0; i < checkboxes.length; ++i) {
297 if (checkboxes[i].checked)
303 const setFilePriority = function(ids, fileIds, priority) {
304 if (current_hash === "")
307 clearTimeout(loadTorrentFilesDataTimer);
308 loadTorrentFilesDataTimer = -1;
311 url: "api/v2/torrents/filePrio",
314 "hash": current_hash,
315 "id": fileIds.join("|"),
318 onComplete: function() {
319 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
323 const ignore = (priority === FilePriority.Ignored);
324 ids.forEach((_id) => {
325 torrentFilesTable.setIgnored(_id, ignore);
327 const combobox = $("comboPrio" + _id);
328 if (combobox !== null)
329 selectComboboxPriority(combobox, priority);
332 torrentFilesTable.updateTable(false);
335 let loadTorrentFilesDataTimer = -1;
336 const loadTorrentFilesData = function() {
337 if ($("propFiles").hasClass("invisible")
338 || $("propertiesPanel_collapseToggle").hasClass("panel-expand")) {
339 // Tab changed, don't do anything
342 const new_hash = torrentsTable.getCurrentTorrentID();
343 if (new_hash === "") {
344 torrentFilesTable.clear();
345 clearTimeout(loadTorrentFilesDataTimer);
346 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
349 let loadedNewTorrent = false;
350 if (new_hash !== current_hash) {
351 torrentFilesTable.clear();
352 current_hash = new_hash;
353 loadedNewTorrent = true;
355 const url = new URI("api/v2/torrents/files?hash=" + current_hash);
360 onComplete: function() {
361 clearTimeout(loadTorrentFilesDataTimer);
362 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
364 onSuccess: function(files) {
365 clearTimeout(torrentFilesFilterInputTimer);
366 torrentFilesFilterInputTimer = -1;
368 if (files.length === 0) {
369 torrentFilesTable.clear();
372 handleNewTorrentFiles(files);
373 if (loadedNewTorrent)
380 const updateData = function() {
381 clearTimeout(loadTorrentFilesDataTimer);
382 loadTorrentFilesDataTimer = -1;
383 loadTorrentFilesData();
386 const handleNewTorrentFiles = function(files) {
387 is_seed = (files.length > 0) ? files[0].is_seed : true;
389 const rows = files.map((file, index) => {
390 let progress = (file.progress * 100).round(1);
391 if ((progress === 100) && (file.progress < 1))
394 const ignore = (file.priority === FilePriority.Ignored);
395 const checked = (ignore ? TriState.Unchecked : TriState.Checked);
396 const remaining = (ignore ? 0 : (file.size * (1.0 - file.progress)));
401 name: window.qBittorrent.Filesystem.fileName(file.name),
404 priority: normalizePriority(file.priority),
405 remaining: remaining,
406 availability: file.availability
412 addRowsToTable(rows);
413 updateGlobalCheckbox();
416 const addRowsToTable = function(rows) {
417 const selectedFiles = torrentFilesTable.selectedRowsIds();
420 const rootNode = new window.qBittorrent.FileTree.FolderNode();
422 rows.forEach((row) => {
423 const pathItems = row.fileName.split(window.qBittorrent.Filesystem.PathSeparator);
425 pathItems.pop(); // remove last item (i.e. file name)
426 let parent = rootNode;
427 pathItems.forEach((folderName) => {
428 if (folderName === ".unwanted")
431 let folderNode = null;
432 if (parent.children !== null) {
433 for (let i = 0; i < parent.children.length; ++i) {
434 const childFolder = parent.children[i];
435 if (childFolder.name === folderName) {
436 folderNode = childFolder;
442 if (folderNode === null) {
443 folderNode = new window.qBittorrent.FileTree.FolderNode();
444 folderNode.path = (parent.path === "")
446 : [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator);
447 folderNode.name = folderName;
448 folderNode.rowId = rowId;
449 folderNode.root = parent;
450 parent.addChild(folderNode);
458 const isChecked = row.checked ? TriState.Checked : TriState.Unchecked;
459 const remaining = (row.priority === FilePriority.Ignored) ? 0 : row.remaining;
460 const childNode = new window.qBittorrent.FileTree.FileNode();
461 childNode.name = row.name;
462 childNode.path = row.fileName;
463 childNode.rowId = rowId;
464 childNode.size = row.size;
465 childNode.checked = isChecked;
466 childNode.remaining = remaining;
467 childNode.progress = row.progress;
468 childNode.priority = row.priority;
469 childNode.availability = row.availability;
470 childNode.root = parent;
471 childNode.data = row;
472 parent.addChild(childNode);
477 torrentFilesTable.populateTable(rootNode);
478 torrentFilesTable.updateTable(false);
480 if (selectedFiles.length > 0)
481 torrentFilesTable.reselectRows(selectedFiles);
484 const collapseIconClicked = function(event) {
485 const id = event.getAttribute("data-id");
486 const node = torrentFilesTable.getNode(id);
487 const isCollapsed = (event.parentElement.getAttribute("data-collapsed") === "true");
495 const expandFolder = function(id) {
496 const node = torrentFilesTable.getNode(id);
501 const collapseFolder = function(id) {
502 const node = torrentFilesTable.getNode(id);
507 const filesPriorityMenuClicked = function(priority) {
508 const selectedRows = torrentFilesTable.selectedRowsIds();
509 if (selectedRows.length === 0)
514 selectedRows.forEach((rowId) => {
515 const elem = $("comboPrio" + rowId);
517 fileIds.push(elem.getAttribute("data-file-id"));
520 const uniqueRowIds = {};
521 const uniqueFileIds = {};
522 for (let i = 0; i < rowIds.length; ++i) {
523 const rows = getAllChildren(rowIds[i], fileIds[i]);
524 rows.rowIds.forEach((rowId) => {
525 uniqueRowIds[rowId] = true;
527 rows.fileIds.forEach((fileId) => {
528 uniqueFileIds[fileId] = true;
532 setFilePriority(Object.keys(uniqueRowIds), Object.keys(uniqueFileIds), priority);
535 const singleFileRename = function(hash) {
536 const rowId = torrentFilesTable.selectedRowsIds()[0];
537 if (rowId === undefined)
539 const row = torrentFilesTable.rows[rowId];
543 const node = torrentFilesTable.getNode(rowId);
544 const path = node.path;
548 icon: "images/qbittorrent-tray.svg",
549 title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
550 loadMethod: "iframe",
551 contentURL: "rename_file.html?hash=" + hash + "&isFolder=" + node.isFolder
552 + "&path=" + encodeURIComponent(path),
557 paddingHorizontal: 0,
563 const multiFileRename = function(hash) {
565 id: "multiRenamePage",
566 icon: "images/qbittorrent-tray.svg",
567 title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
568 data: { hash: hash, selectedRows: torrentFilesTable.selectedRows },
570 contentURL: "rename_files.html",
575 paddingHorizontal: 0,
578 resizeLimit: { "x": [800], "y": [420] }
582 const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
583 targets: "#torrentFilesTableDiv tr",
584 menu: "torrentFilesMenu",
586 Rename: function(element, ref) {
587 const hash = torrentsTable.getCurrentTorrentID();
591 if (torrentFilesTable.selectedRowsIds().length > 1)
592 multiFileRename(hash);
594 singleFileRename(hash);
597 FilePrioIgnore: function(element, ref) {
598 filesPriorityMenuClicked(FilePriority.Ignored);
600 FilePrioNormal: function(element, ref) {
601 filesPriorityMenuClicked(FilePriority.Normal);
603 FilePrioHigh: function(element, ref) {
604 filesPriorityMenuClicked(FilePriority.High);
606 FilePrioMaximum: function(element, ref) {
607 filesPriorityMenuClicked(FilePriority.Maximum);
616 this.hideItem("FilePrio");
618 this.showItem("FilePrio");
622 torrentFilesTable.setup("torrentFilesTableDiv", "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu);
623 // inject checkbox into table header
624 const tableHeaders = $$("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
625 if (tableHeaders.length > 0) {
626 const checkbox = new Element("input");
627 checkbox.type = "checkbox";
628 checkbox.id = "tristate_cb";
629 checkbox.addEventListener("click", switchCheckboxState);
631 const checkboxTH = tableHeaders[0];
632 checkbox.injectInside(checkboxTH);
635 // default sort by name column
636 if (torrentFilesTable.getSortedColumn() === null)
637 torrentFilesTable.setSortedColumn("name");
639 // listen for changes to torrentFilesFilterInput
640 let torrentFilesFilterInputTimer = -1;
641 $("torrentFilesFilterInput").addEventListener("input", () => {
642 clearTimeout(torrentFilesFilterInputTimer);
644 const value = $("torrentFilesFilterInput").value;
645 torrentFilesTable.setFilter(value);
647 torrentFilesFilterInputTimer = setTimeout(() => {
648 torrentFilesFilterInputTimer = -1;
650 if (current_hash === "")
653 torrentFilesTable.updateTable();
655 if (value.trim() === "")
659 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
663 * Show/hide a node's row
665 const _hideNode = function(node, shouldHide) {
666 const span = $("filesTablefileName" + node.rowId);
667 // span won't exist if row has been filtered out
670 const rowElem = span.parentElement.parentElement;
672 rowElem.addClass("invisible");
674 rowElem.removeClass("invisible");
678 * Update a node's collapsed state and icon
680 const _updateNodeState = function(node, isCollapsed) {
681 const span = $("filesTablefileName" + node.rowId);
682 // span won't exist if row has been filtered out
685 const td = span.parentElement;
687 // store collapsed state
688 td.setAttribute("data-collapsed", isCollapsed);
690 // rotate the collapse icon
691 const collapseIcon = td.getElementsByClassName("filesTableCollapseIcon")[0];
693 collapseIcon.addClass("rotate");
695 collapseIcon.removeClass("rotate");
698 const _isCollapsed = function(node) {
699 const span = $("filesTablefileName" + node.rowId);
703 const td = span.parentElement;
704 return td.getAttribute("data-collapsed") === "true";
707 const expandNode = function(node) {
708 _collapseNode(node, false, false, false);
711 const collapseNode = function(node) {
712 _collapseNode(node, true, false, false);
715 const expandAllNodes = function() {
716 const root = torrentFilesTable.getRoot();
717 root.children.each((node) => {
718 node.children.each((child) => {
719 _collapseNode(child, false, true, false);
724 const collapseAllNodes = function() {
725 const root = torrentFilesTable.getRoot();
726 root.children.each((node) => {
727 node.children.each((child) => {
728 _collapseNode(child, true, true, false);
734 * Collapses a folder node with the option to recursively collapse all children
735 * @param {FolderNode} node the node to collapse/expand
736 * @param {boolean} shouldCollapse true if the node should be collapsed, false if it should be expanded
737 * @param {boolean} applyToChildren true if the node's children should also be collapsed, recursively
738 * @param {boolean} isChildNode true if the current node is a child of the original node we collapsed/expanded
740 const _collapseNode = function(node, shouldCollapse, applyToChildren, isChildNode) {
744 const shouldExpand = !shouldCollapse;
745 const isNodeCollapsed = _isCollapsed(node);
746 const nodeInCorrectState = ((shouldCollapse && isNodeCollapsed) || (shouldExpand && !isNodeCollapsed));
747 const canSkipNode = (isChildNode && (!applyToChildren || nodeInCorrectState));
748 if (!isChildNode || applyToChildren || !canSkipNode)
749 _updateNodeState(node, shouldCollapse);
751 node.children.each((child) => {
752 _hideNode(child, shouldCollapse);
757 // don't expand children that have been independently collapsed, unless applyToChildren is true
758 const shouldExpandChildren = (shouldExpand && applyToChildren);
759 const isChildCollapsed = _isCollapsed(child);
760 if (!shouldExpandChildren && isChildCollapsed)
763 _collapseNode(child, shouldCollapse, applyToChildren, true);
769 Object.freeze(window.qBittorrent.PropFiles);