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