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
.addEvent("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 createPriorityOptionElement = function(priority
, selected
, html
) {
169 const elem
= new Element("option");
170 elem
.value
= priority
.toString();
171 elem
.innerHTML
= html
;
173 elem
.selected
= true;
177 const createPriorityCombo = function(id
, fileId
, selectedPriority
) {
178 const select
= new Element("select");
179 select
.id
= "comboPrio" + id
;
180 select
.setAttribute("data-id", id
);
181 select
.setAttribute("data-file-id", fileId
);
182 select
.addClass("combo_priority");
183 select
.addEvent("change", fileComboboxChanged
);
185 createPriorityOptionElement(FilePriority
.Ignored
, (FilePriority
.Ignored
=== selectedPriority
), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select
);
186 createPriorityOptionElement(FilePriority
.Normal
, (FilePriority
.Normal
=== selectedPriority
), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select
);
187 createPriorityOptionElement(FilePriority
.High
, (FilePriority
.High
=== selectedPriority
), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select
);
188 createPriorityOptionElement(FilePriority
.Maximum
, (FilePriority
.Maximum
=== selectedPriority
), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select
);
190 // "Mixed" priority is for display only; it shouldn't be selectable
191 const mixedPriorityOption
= createPriorityOptionElement(FilePriority
.Mixed
, (FilePriority
.Mixed
=== selectedPriority
), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
192 mixedPriorityOption
.disabled
= true;
193 mixedPriorityOption
.injectInside(select
);
198 const updatePriorityCombo = function(id
, selectedPriority
) {
199 const combobox
= $("comboPrio" + id
);
200 if (parseInt(combobox
.value
, 10) !== selectedPriority
)
201 selectComboboxPriority(combobox
, selectedPriority
);
204 const selectComboboxPriority = function(combobox
, priority
) {
205 const options
= combobox
.options
;
206 for (let i
= 0; i
< options
.length
; ++i
) {
207 const option
= options
[i
];
208 if (parseInt(option
.value
, 10) === priority
)
209 option
.selected
= true;
211 option
.selected
= false;
214 combobox
.value
= priority
;
217 const switchCheckboxState = function(e
) {
222 let priority
= FilePriority
.Ignored
;
223 const checkbox
= $("tristate_cb");
225 if (checkbox
.state
=== "checked") {
226 setCheckboxUnchecked(checkbox
);
227 // set file priority for all checked to Ignored
228 torrentFilesTable
.getFilteredAndSortedRows().forEach((row
) => {
229 const rowId
= row
.rowId
;
230 const fileId
= row
.full_data
.fileId
;
231 const isChecked
= (row
.full_data
.checked
=== TriState
.Checked
);
232 const isFolder
= (fileId
=== -1);
233 if (!isFolder
&& isChecked
) {
235 fileIds
.push(fileId
);
240 setCheckboxChecked(checkbox
);
241 priority
= FilePriority
.Normal
;
242 // set file priority for all unchecked to Normal
243 torrentFilesTable
.getFilteredAndSortedRows().forEach((row
) => {
244 const rowId
= row
.rowId
;
245 const fileId
= row
.full_data
.fileId
;
246 const isUnchecked
= (row
.full_data
.checked
=== TriState
.Unchecked
);
247 const isFolder
= (fileId
=== -1);
248 if (!isFolder
&& isUnchecked
) {
250 fileIds
.push(fileId
);
255 if (rowIds
.length
> 0)
256 setFilePriority(rowIds
, fileIds
, priority
);
259 const updateGlobalCheckbox = function() {
260 const checkbox
= $("tristate_cb");
261 if (isAllCheckboxesChecked())
262 setCheckboxChecked(checkbox
);
263 else if (isAllCheckboxesUnchecked())
264 setCheckboxUnchecked(checkbox
);
266 setCheckboxPartial(checkbox
);
269 const setCheckboxChecked = function(checkbox
) {
270 checkbox
.state
= "checked";
271 checkbox
.indeterminate
= false;
272 checkbox
.checked
= true;
275 const setCheckboxUnchecked = function(checkbox
) {
276 checkbox
.state
= "unchecked";
277 checkbox
.indeterminate
= false;
278 checkbox
.checked
= false;
281 const setCheckboxPartial = function(checkbox
) {
282 checkbox
.state
= "partial";
283 checkbox
.indeterminate
= true;
286 const isAllCheckboxesChecked = function() {
287 const checkboxes
= $$("input.DownloadedCB");
288 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
289 if (!checkboxes
[i
].checked
)
295 const isAllCheckboxesUnchecked = function() {
296 const checkboxes
= $$("input.DownloadedCB");
297 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
298 if (checkboxes
[i
].checked
)
304 const setFilePriority = function(ids
, fileIds
, priority
) {
305 if (current_hash
=== "")
308 clearTimeout(loadTorrentFilesDataTimer
);
310 url
: "api/v2/torrents/filePrio",
313 "hash": current_hash
,
314 "id": fileIds
.join("|"),
317 onComplete: function() {
318 loadTorrentFilesDataTimer
= loadTorrentFilesData
.delay(1000);
322 const ignore
= (priority
=== FilePriority
.Ignored
);
323 ids
.forEach((_id
) => {
324 torrentFilesTable
.setIgnored(_id
, ignore
);
326 const combobox
= $("comboPrio" + _id
);
327 if (combobox
!== null)
328 selectComboboxPriority(combobox
, priority
);
331 torrentFilesTable
.updateTable(false);
334 let loadTorrentFilesDataTimer
;
335 const loadTorrentFilesData = function() {
336 if ($("prop_files").hasClass("invisible")
337 || $("propertiesPanel_collapseToggle").hasClass("panel-expand")) {
338 // Tab changed, don't do anything
341 const new_hash
= torrentsTable
.getCurrentTorrentID();
342 if (new_hash
=== "") {
343 torrentFilesTable
.clear();
344 clearTimeout(loadTorrentFilesDataTimer
);
345 loadTorrentFilesDataTimer
= loadTorrentFilesData
.delay(5000);
348 let loadedNewTorrent
= false;
349 if (new_hash
!== current_hash
) {
350 torrentFilesTable
.clear();
351 current_hash
= new_hash
;
352 loadedNewTorrent
= true;
354 const url
= new URI("api/v2/torrents/files?hash=" + current_hash
);
359 onComplete: function() {
360 clearTimeout(loadTorrentFilesDataTimer
);
361 loadTorrentFilesDataTimer
= loadTorrentFilesData
.delay(5000);
363 onSuccess: function(files
) {
364 clearTimeout(torrentFilesFilterInputTimer
);
365 torrentFilesFilterInputTimer
= -1;
367 if (files
.length
=== 0) {
368 torrentFilesTable
.clear();
371 handleNewTorrentFiles(files
);
372 if (loadedNewTorrent
)
379 const updateData = function() {
380 clearTimeout(loadTorrentFilesDataTimer
);
381 loadTorrentFilesData();
384 const handleNewTorrentFiles = function(files
) {
385 is_seed
= (files
.length
> 0) ? files
[0].is_seed
: true;
387 const rows
= files
.map((file
, index
) => {
388 let progress
= (file
.progress
* 100).round(1);
389 if ((progress
=== 100) && (file
.progress
< 1))
392 const ignore
= (file
.priority
=== FilePriority
.Ignored
);
393 const checked
= (ignore
? TriState
.Unchecked
: TriState
.Checked
);
394 const remaining
= (ignore
? 0 : (file
.size
* (1.0 - file
.progress
)));
399 name
: window
.qBittorrent
.Filesystem
.fileName(file
.name
),
402 priority
: normalizePriority(file
.priority
),
403 remaining
: remaining
,
404 availability
: file
.availability
410 addRowsToTable(rows
);
411 updateGlobalCheckbox();
414 const addRowsToTable = function(rows
) {
415 const selectedFiles
= torrentFilesTable
.selectedRowsIds();
418 const rootNode
= new window
.qBittorrent
.FileTree
.FolderNode();
420 rows
.forEach((row
) => {
421 const pathItems
= row
.fileName
.split(window
.qBittorrent
.Filesystem
.PathSeparator
);
423 pathItems
.pop(); // remove last item (i.e. file name)
424 let parent
= rootNode
;
425 pathItems
.forEach((folderName
) => {
426 if (folderName
=== ".unwanted")
429 let folderNode
= null;
430 if (parent
.children
!== null) {
431 for (let i
= 0; i
< parent
.children
.length
; ++i
) {
432 const childFolder
= parent
.children
[i
];
433 if (childFolder
.name
=== folderName
) {
434 folderNode
= childFolder
;
440 if (folderNode
=== null) {
441 folderNode
= new window
.qBittorrent
.FileTree
.FolderNode();
442 folderNode
.path
= (parent
.path
=== "")
444 : [parent
.path
, folderName
].join(window
.qBittorrent
.Filesystem
.PathSeparator
);
445 folderNode
.name
= folderName
;
446 folderNode
.rowId
= rowId
;
447 folderNode
.root
= parent
;
448 parent
.addChild(folderNode
);
456 const isChecked
= row
.checked
? TriState
.Checked
: TriState
.Unchecked
;
457 const remaining
= (row
.priority
=== FilePriority
.Ignored
) ? 0 : row
.remaining
;
458 const childNode
= new window
.qBittorrent
.FileTree
.FileNode();
459 childNode
.name
= row
.name
;
460 childNode
.path
= row
.fileName
;
461 childNode
.rowId
= rowId
;
462 childNode
.size
= row
.size
;
463 childNode
.checked
= isChecked
;
464 childNode
.remaining
= remaining
;
465 childNode
.progress
= row
.progress
;
466 childNode
.priority
= row
.priority
;
467 childNode
.availability
= row
.availability
;
468 childNode
.root
= parent
;
469 childNode
.data
= row
;
470 parent
.addChild(childNode
);
475 torrentFilesTable
.populateTable(rootNode
);
476 torrentFilesTable
.updateTable(false);
478 if (selectedFiles
.length
> 0)
479 torrentFilesTable
.reselectRows(selectedFiles
);
482 const collapseIconClicked = function(event
) {
483 const id
= event
.getAttribute("data-id");
484 const node
= torrentFilesTable
.getNode(id
);
485 const isCollapsed
= (event
.parentElement
.getAttribute("data-collapsed") === "true");
493 const expandFolder = function(id
) {
494 const node
= torrentFilesTable
.getNode(id
);
499 const collapseFolder = function(id
) {
500 const node
= torrentFilesTable
.getNode(id
);
505 const filesPriorityMenuClicked = function(priority
) {
506 const selectedRows
= torrentFilesTable
.selectedRowsIds();
507 if (selectedRows
.length
=== 0)
512 selectedRows
.forEach((rowId
) => {
513 const elem
= $("comboPrio" + rowId
);
515 fileIds
.push(elem
.getAttribute("data-file-id"));
518 const uniqueRowIds
= {};
519 const uniqueFileIds
= {};
520 for (let i
= 0; i
< rowIds
.length
; ++i
) {
521 const rows
= getAllChildren(rowIds
[i
], fileIds
[i
]);
522 rows
.rowIds
.forEach((rowId
) => {
523 uniqueRowIds
[rowId
] = true;
525 rows
.fileIds
.forEach((fileId
) => {
526 uniqueFileIds
[fileId
] = true;
530 setFilePriority(Object
.keys(uniqueRowIds
), Object
.keys(uniqueFileIds
), priority
);
533 const singleFileRename = function(hash
) {
534 const rowId
= torrentFilesTable
.selectedRowsIds()[0];
535 if (rowId
=== undefined)
537 const row
= torrentFilesTable
.rows
[rowId
];
541 const node
= torrentFilesTable
.getNode(rowId
);
542 const path
= node
.path
;
546 title
: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
547 loadMethod
: "iframe",
548 contentURL
: "rename_file.html?hash=" + hash
+ "&isFolder=" + node
.isFolder
549 + "&path=" + encodeURIComponent(path
),
554 paddingHorizontal
: 0,
560 const multiFileRename = function(hash
) {
562 id
: "multiRenamePage",
563 title
: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
564 data
: { hash
: hash
, selectedRows
: torrentFilesTable
.selectedRows
},
566 contentURL
: "rename_files.html",
571 paddingHorizontal
: 0,
574 resizeLimit
: { "x": [800], "y": [420] }
578 const torrentFilesContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
579 targets
: "#torrentFilesTableDiv tr",
580 menu
: "torrentFilesMenu",
582 Rename: function(element
, ref
) {
583 const hash
= torrentsTable
.getCurrentTorrentID();
587 if (torrentFilesTable
.selectedRowsIds().length
> 1)
588 multiFileRename(hash
);
590 singleFileRename(hash
);
593 FilePrioIgnore: function(element
, ref
) {
594 filesPriorityMenuClicked(FilePriority
.Ignored
);
596 FilePrioNormal: function(element
, ref
) {
597 filesPriorityMenuClicked(FilePriority
.Normal
);
599 FilePrioHigh: function(element
, ref
) {
600 filesPriorityMenuClicked(FilePriority
.High
);
602 FilePrioMaximum: function(element
, ref
) {
603 filesPriorityMenuClicked(FilePriority
.Maximum
);
612 this.hideItem("FilePrio");
614 this.showItem("FilePrio");
618 torrentFilesTable
.setup("torrentFilesTableDiv", "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu
);
619 // inject checkbox into table header
620 const tableHeaders
= $$("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
621 if (tableHeaders
.length
> 0) {
622 const checkbox
= new Element("input");
623 checkbox
.type
= "checkbox";
624 checkbox
.id
= "tristate_cb";
625 checkbox
.addEvent("click", switchCheckboxState
);
627 const checkboxTH
= tableHeaders
[0];
628 checkbox
.injectInside(checkboxTH
);
631 // default sort by name column
632 if (torrentFilesTable
.getSortedColumn() === null)
633 torrentFilesTable
.setSortedColumn("name");
635 // listen for changes to torrentFilesFilterInput
636 let torrentFilesFilterInputTimer
= -1;
637 $("torrentFilesFilterInput").addEvent("input", () => {
638 clearTimeout(torrentFilesFilterInputTimer
);
640 const value
= $("torrentFilesFilterInput").value
;
641 torrentFilesTable
.setFilter(value
);
643 torrentFilesFilterInputTimer
= setTimeout(() => {
644 torrentFilesFilterInputTimer
= -1;
646 if (current_hash
=== "")
649 torrentFilesTable
.updateTable();
651 if (value
.trim() === "")
655 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
659 * Show/hide a node's row
661 const _hideNode = function(node
, shouldHide
) {
662 const span
= $("filesTablefileName" + node
.rowId
);
663 // span won't exist if row has been filtered out
666 const rowElem
= span
.parentElement
.parentElement
;
668 rowElem
.addClass("invisible");
670 rowElem
.removeClass("invisible");
674 * Update a node's collapsed state and icon
676 const _updateNodeState = function(node
, isCollapsed
) {
677 const span
= $("filesTablefileName" + node
.rowId
);
678 // span won't exist if row has been filtered out
681 const td
= span
.parentElement
;
683 // store collapsed state
684 td
.setAttribute("data-collapsed", isCollapsed
);
686 // rotate the collapse icon
687 const collapseIcon
= td
.getElementsByClassName("filesTableCollapseIcon")[0];
689 collapseIcon
.addClass("rotate");
691 collapseIcon
.removeClass("rotate");
694 const _isCollapsed = function(node
) {
695 const span
= $("filesTablefileName" + node
.rowId
);
699 const td
= span
.parentElement
;
700 return td
.getAttribute("data-collapsed") === "true";
703 const expandNode = function(node
) {
704 _collapseNode(node
, false, false, false);
707 const collapseNode = function(node
) {
708 _collapseNode(node
, true, false, false);
711 const expandAllNodes = function() {
712 const root
= torrentFilesTable
.getRoot();
713 root
.children
.each((node
) => {
714 node
.children
.each((child
) => {
715 _collapseNode(child
, false, true, false);
720 const collapseAllNodes = function() {
721 const root
= torrentFilesTable
.getRoot();
722 root
.children
.each((node
) => {
723 node
.children
.each((child
) => {
724 _collapseNode(child
, true, true, false);
730 * Collapses a folder node with the option to recursively collapse all children
731 * @param {FolderNode} node the node to collapse/expand
732 * @param {boolean} shouldCollapse true if the node should be collapsed, false if it should be expanded
733 * @param {boolean} applyToChildren true if the node's children should also be collapsed, recursively
734 * @param {boolean} isChildNode true if the current node is a child of the original node we collapsed/expanded
736 const _collapseNode = function(node
, shouldCollapse
, applyToChildren
, isChildNode
) {
740 const shouldExpand
= !shouldCollapse
;
741 const isNodeCollapsed
= _isCollapsed(node
);
742 const nodeInCorrectState
= ((shouldCollapse
&& isNodeCollapsed
) || (shouldExpand
&& !isNodeCollapsed
));
743 const canSkipNode
= (isChildNode
&& (!applyToChildren
|| nodeInCorrectState
));
744 if (!isChildNode
|| applyToChildren
|| !canSkipNode
)
745 _updateNodeState(node
, shouldCollapse
);
747 node
.children
.each((child
) => {
748 _hideNode(child
, shouldCollapse
);
753 // don't expand children that have been independently collapsed, unless applyToChildren is true
754 const shouldExpandChildren
= (shouldExpand
&& applyToChildren
);
755 const isChildCollapsed
= _isCollapsed(child
);
756 if (!shouldExpandChildren
&& isChildCollapsed
)
759 _collapseNode(child
, shouldCollapse
, applyToChildren
, true);
765 Object
.freeze(window
.qBittorrent
.PropFiles
);