Display External IP Address in status bar
[qBittorrent.git] / src / webui / www / private / scripts / prop-files.js
bloba00ece320f2c4c8dbab7b5717323c790d600ea9e
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2009  Christophe Dumez <chris@qbittorrent.org>
4  *
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.
9  *
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.
14  *
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.
18  *
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.
27  */
29 "use strict";
31 window.qBittorrent ??= {};
32 window.qBittorrent.PropFiles ??= (() => {
33     const exports = () => {
34         return {
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,
46             clear: clear
47         };
48     };
50     const torrentFilesTable = new window.qBittorrent.DynamicTable.TorrentFilesTable();
51     const FilePriority = window.qBittorrent.FileTree.FilePriority;
52     const TriState = window.qBittorrent.FileTree.TriState;
53     let is_seed = true;
54     let current_hash = "";
56     const normalizePriority = (priority) => {
57         switch (priority) {
58             case FilePriority.Ignored:
59             case FilePriority.Normal:
60             case FilePriority.High:
61             case FilePriority.Maximum:
62             case FilePriority.Mixed:
63                 return priority;
64             default:
65                 return FilePriority.Normal;
66         }
67     };
69     const getAllChildren = (id, fileId) => {
70         const node = torrentFilesTable.getNode(id);
71         if (!node.isFolder) {
72             return {
73                 rowIds: [id],
74                 fileIds: [fileId]
75             };
76         }
78         const rowIds = [];
79         const fileIds = [];
81         const getChildFiles = (node) => {
82             if (node.isFolder) {
83                 node.children.each((child) => {
84                     getChildFiles(child);
85                 });
86             }
87             else {
88                 rowIds.push(node.data.rowId);
89                 fileIds.push(node.data.fileId);
90             }
91         };
93         node.children.each((child) => {
94             getChildFiles(child);
95         });
97         return {
98             rowIds: rowIds,
99             fileIds: fileIds
100         };
101     };
103     const fileCheckboxClicked = (e) => {
104         e.stopPropagation();
106         const checkbox = e.target;
107         const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored;
108         const id = checkbox.getAttribute("data-id");
109         const fileId = checkbox.getAttribute("data-file-id");
111         const rows = getAllChildren(id, fileId);
113         setFilePriority(rows.rowIds, rows.fileIds, priority);
114         updateGlobalCheckbox();
115     };
117     const fileComboboxChanged = (e) => {
118         const combobox = e.target;
119         const priority = combobox.value;
120         const id = combobox.getAttribute("data-id");
121         const fileId = combobox.getAttribute("data-file-id");
123         const rows = getAllChildren(id, fileId);
125         setFilePriority(rows.rowIds, rows.fileIds, priority);
126         updateGlobalCheckbox();
127     };
129     const isDownloadCheckboxExists = (id) => {
130         return $("cbPrio" + id) !== null;
131     };
133     const createDownloadCheckbox = (id, fileId, checked) => {
134         const checkbox = new Element("input");
135         checkbox.type = "checkbox";
136         checkbox.id = "cbPrio" + id;
137         checkbox.setAttribute("data-id", id);
138         checkbox.setAttribute("data-file-id", fileId);
139         checkbox.className = "DownloadedCB";
140         checkbox.addEventListener("click", fileCheckboxClicked);
142         updateCheckbox(checkbox, checked);
143         return checkbox;
144     };
146     const updateDownloadCheckbox = (id, checked) => {
147         const checkbox = $("cbPrio" + id);
148         updateCheckbox(checkbox, checked);
149     };
151     const updateCheckbox = (checkbox, checked) => {
152         switch (checked) {
153             case TriState.Checked:
154                 setCheckboxChecked(checkbox);
155                 break;
156             case TriState.Unchecked:
157                 setCheckboxUnchecked(checkbox);
158                 break;
159             case TriState.Partial:
160                 setCheckboxPartial(checkbox);
161                 break;
162         }
163     };
165     const isPriorityComboExists = (id) => {
166         return $("comboPrio" + id) !== null;
167     };
169     const createPriorityCombo = (id, fileId, selectedPriority) => {
170         const createOption = (priority, isSelected, text) => {
171             const option = document.createElement("option");
172             option.value = priority.toString();
173             option.selected = isSelected;
174             option.textContent = text;
175             return option;
176         };
178         const select = document.createElement("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.addEventListener("change", fileComboboxChanged);
185         select.appendChild(createOption(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]"));
186         select.appendChild(createOption(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]"));
187         select.appendChild(createOption(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]"));
188         select.appendChild(createOption(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]"));
190         // "Mixed" priority is for display only; it shouldn't be selectable
191         const mixedPriorityOption = createOption(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
192         mixedPriorityOption.disabled = true;
193         select.appendChild(mixedPriorityOption);
195         return select;
196     };
198     const updatePriorityCombo = (id, selectedPriority) => {
199         const combobox = $("comboPrio" + id);
200         if (parseInt(combobox.value, 10) !== selectedPriority)
201             selectComboboxPriority(combobox, selectedPriority);
202     };
204     const selectComboboxPriority = (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;
210             else
211                 option.selected = false;
212         }
214         combobox.value = priority;
215     };
217     const switchCheckboxState = (e) => {
218         e.stopPropagation();
220         const rowIds = [];
221         const fileIds = [];
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) {
234                     rowIds.push(rowId);
235                     fileIds.push(fileId);
236                 }
237             });
238         }
239         else {
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) {
249                     rowIds.push(rowId);
250                     fileIds.push(fileId);
251                 }
252             });
253         }
255         if (rowIds.length > 0)
256             setFilePriority(rowIds, fileIds, priority);
257     };
259     const updateGlobalCheckbox = () => {
260         const checkbox = $("tristate_cb");
261         if (isAllCheckboxesChecked())
262             setCheckboxChecked(checkbox);
263         else if (isAllCheckboxesUnchecked())
264             setCheckboxUnchecked(checkbox);
265         else
266             setCheckboxPartial(checkbox);
267     };
269     const setCheckboxChecked = (checkbox) => {
270         checkbox.state = "checked";
271         checkbox.indeterminate = false;
272         checkbox.checked = true;
273     };
275     const setCheckboxUnchecked = (checkbox) => {
276         checkbox.state = "unchecked";
277         checkbox.indeterminate = false;
278         checkbox.checked = false;
279     };
281     const setCheckboxPartial = (checkbox) => {
282         checkbox.state = "partial";
283         checkbox.indeterminate = true;
284     };
286     const isAllCheckboxesChecked = () => {
287         const checkboxes = $$("input.DownloadedCB");
288         for (let i = 0; i < checkboxes.length; ++i) {
289             if (!checkboxes[i].checked)
290                 return false;
291         }
292         return true;
293     };
295     const isAllCheckboxesUnchecked = () => {
296         const checkboxes = $$("input.DownloadedCB");
297         for (let i = 0; i < checkboxes.length; ++i) {
298             if (checkboxes[i].checked)
299                 return false;
300         }
301         return true;
302     };
304     const setFilePriority = (ids, fileIds, priority) => {
305         if (current_hash === "")
306             return;
308         clearTimeout(loadTorrentFilesDataTimer);
309         loadTorrentFilesDataTimer = -1;
311         new Request({
312             url: "api/v2/torrents/filePrio",
313             method: "post",
314             data: {
315                 "hash": current_hash,
316                 "id": fileIds.join("|"),
317                 "priority": priority
318             },
319             onComplete: () => {
320                 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
321             }
322         }).send();
324         const ignore = (priority === FilePriority.Ignored);
325         ids.forEach((_id) => {
326             torrentFilesTable.setIgnored(_id, ignore);
328             const combobox = $("comboPrio" + _id);
329             if (combobox !== null)
330                 selectComboboxPriority(combobox, priority);
331         });
333         torrentFilesTable.updateTable(false);
334     };
336     let loadTorrentFilesDataTimer = -1;
337     const loadTorrentFilesData = () => {
338         if ($("propFiles").hasClass("invisible")
339             || $("propertiesPanel_collapseToggle").hasClass("panel-expand")) {
340             // Tab changed, don't do anything
341             return;
342         }
343         const new_hash = torrentsTable.getCurrentTorrentID();
344         if (new_hash === "") {
345             torrentFilesTable.clear();
346             clearTimeout(loadTorrentFilesDataTimer);
347             return;
348         }
349         let loadedNewTorrent = false;
350         if (new_hash !== current_hash) {
351             torrentFilesTable.clear();
352             current_hash = new_hash;
353             loadedNewTorrent = true;
354         }
355         const url = new URI("api/v2/torrents/files?hash=" + current_hash);
356         new Request.JSON({
357             url: url,
358             method: "get",
359             noCache: true,
360             onComplete: () => {
361                 clearTimeout(loadTorrentFilesDataTimer);
362                 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
363             },
364             onSuccess: (files) => {
365                 clearTimeout(torrentFilesFilterInputTimer);
366                 torrentFilesFilterInputTimer = -1;
368                 if (files.length === 0) {
369                     torrentFilesTable.clear();
370                 }
371                 else {
372                     handleNewTorrentFiles(files);
373                     if (loadedNewTorrent)
374                         collapseAllNodes();
375                 }
376             }
377         }).send();
378     };
380     const updateData = () => {
381         clearTimeout(loadTorrentFilesDataTimer);
382         loadTorrentFilesDataTimer = -1;
383         loadTorrentFilesData();
384     };
386     const handleNewTorrentFiles = (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))
392                 progress = 99.9;
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)));
397             const row = {
398                 fileId: index,
399                 checked: checked,
400                 fileName: file.name,
401                 name: window.qBittorrent.Filesystem.fileName(file.name),
402                 size: file.size,
403                 progress: progress,
404                 priority: normalizePriority(file.priority),
405                 remaining: remaining,
406                 availability: file.availability
407             };
409             return row;
410         });
412         addRowsToTable(rows);
413         updateGlobalCheckbox();
414     };
416     const addRowsToTable = (rows) => {
417         const selectedFiles = torrentFilesTable.selectedRowsIds();
418         let rowId = 0;
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")
429                     return;
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;
437                             break;
438                         }
439                     }
440                 }
442                 if (folderNode === null) {
443                     folderNode = new window.qBittorrent.FileTree.FolderNode();
444                     folderNode.path = (parent.path === "")
445                         ? folderName
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);
452                     ++rowId;
453                 }
455                 parent = folderNode;
456             });
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);
474             ++rowId;
475         });
477         torrentFilesTable.populateTable(rootNode);
478         torrentFilesTable.updateTable(false);
480         if (selectedFiles.length > 0)
481             torrentFilesTable.reselectRows(selectedFiles);
482     };
484     const collapseIconClicked = (event) => {
485         const id = event.getAttribute("data-id");
486         const node = torrentFilesTable.getNode(id);
487         const isCollapsed = (event.parentElement.getAttribute("data-collapsed") === "true");
489         if (isCollapsed)
490             expandNode(node);
491         else
492             collapseNode(node);
493     };
495     const expandFolder = (id) => {
496         const node = torrentFilesTable.getNode(id);
497         if (node.isFolder)
498             expandNode(node);
499     };
501     const collapseFolder = (id) => {
502         const node = torrentFilesTable.getNode(id);
503         if (node.isFolder)
504             collapseNode(node);
505     };
507     const filesPriorityMenuClicked = (priority) => {
508         const selectedRows = torrentFilesTable.selectedRowsIds();
509         if (selectedRows.length === 0)
510             return;
512         const rowIds = [];
513         const fileIds = [];
514         selectedRows.forEach((rowId) => {
515             const elem = $("comboPrio" + rowId);
516             rowIds.push(rowId);
517             fileIds.push(elem.getAttribute("data-file-id"));
518         });
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;
526             });
527             rows.fileIds.forEach((fileId) => {
528                 uniqueFileIds[fileId] = true;
529             });
530         }
532         setFilePriority(Object.keys(uniqueRowIds), Object.keys(uniqueFileIds), priority);
533     };
535     const singleFileRename = (hash) => {
536         const rowId = torrentFilesTable.selectedRowsIds()[0];
537         if (rowId === undefined)
538             return;
539         const row = torrentFilesTable.rows.get(rowId);
540         if (!row)
541             return;
543         const node = torrentFilesTable.getNode(rowId);
544         const path = node.path;
546         new MochaUI.Window({
547             id: "renamePage",
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),
553             scrollbars: false,
554             resizable: true,
555             maximizable: false,
556             paddingVertical: 0,
557             paddingHorizontal: 0,
558             width: 400,
559             height: 100
560         });
561     };
563     const multiFileRename = (hash) => {
564         new MochaUI.Window({
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 },
569             loadMethod: "xhr",
570             contentURL: "rename_files.html",
571             scrollbars: false,
572             resizable: true,
573             maximizable: false,
574             paddingVertical: 0,
575             paddingHorizontal: 0,
576             width: 800,
577             height: 420,
578             resizeLimit: { "x": [800], "y": [420] }
579         });
580     };
582     const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
583         targets: "#torrentFilesTableDiv tr",
584         menu: "torrentFilesMenu",
585         actions: {
586             Rename: (element, ref) => {
587                 const hash = torrentsTable.getCurrentTorrentID();
588                 if (!hash)
589                     return;
591                 if (torrentFilesTable.selectedRowsIds().length > 1)
592                     multiFileRename(hash);
593                 else
594                     singleFileRename(hash);
595             },
597             FilePrioIgnore: (element, ref) => {
598                 filesPriorityMenuClicked(FilePriority.Ignored);
599             },
600             FilePrioNormal: (element, ref) => {
601                 filesPriorityMenuClicked(FilePriority.Normal);
602             },
603             FilePrioHigh: (element, ref) => {
604                 filesPriorityMenuClicked(FilePriority.High);
605             },
606             FilePrioMaximum: (element, ref) => {
607                 filesPriorityMenuClicked(FilePriority.Maximum);
608             }
609         },
610         offsets: {
611             x: 0,
612             y: 2
613         },
614         onShow: function() {
615             if (is_seed)
616                 this.hideItem("FilePrio");
617             else
618                 this.showItem("FilePrio");
619         }
620     });
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);
633     }
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 === "")
651                 return;
653             torrentFilesTable.updateTable();
655             if (value.trim() === "")
656                 collapseAllNodes();
657             else
658                 expandAllNodes();
659         }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
660     });
662     /**
663      * Show/hide a node's row
664      */
665     const _hideNode = (node, shouldHide) => {
666         const span = $("filesTablefileName" + node.rowId);
667         // span won't exist if row has been filtered out
668         if (span === null)
669             return;
670         const rowElem = span.parentElement.parentElement;
671         if (shouldHide)
672             rowElem.addClass("invisible");
673         else
674             rowElem.removeClass("invisible");
675     };
677     /**
678      * Update a node's collapsed state and icon
679      */
680     const _updateNodeState = (node, isCollapsed) => {
681         const span = $("filesTablefileName" + node.rowId);
682         // span won't exist if row has been filtered out
683         if (span === null)
684             return;
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];
692         if (isCollapsed)
693             collapseIcon.addClass("rotate");
694         else
695             collapseIcon.removeClass("rotate");
696     };
698     const _isCollapsed = (node) => {
699         const span = $("filesTablefileName" + node.rowId);
700         if (span === null)
701             return true;
703         const td = span.parentElement;
704         return td.getAttribute("data-collapsed") === "true";
705     };
707     const expandNode = (node) => {
708         _collapseNode(node, false, false, false);
709     };
711     const collapseNode = (node) => {
712         _collapseNode(node, true, false, false);
713     };
715     const expandAllNodes = () => {
716         const root = torrentFilesTable.getRoot();
717         root.children.each((node) => {
718             node.children.each((child) => {
719                 _collapseNode(child, false, true, false);
720             });
721         });
722     };
724     const collapseAllNodes = () => {
725         const root = torrentFilesTable.getRoot();
726         root.children.each((node) => {
727             node.children.each((child) => {
728                 _collapseNode(child, true, true, false);
729             });
730         });
731     };
733     /**
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
739      */
740     const _collapseNode = (node, shouldCollapse, applyToChildren, isChildNode) => {
741         if (!node.isFolder)
742             return;
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);
754             if (!child.isFolder)
755                 return;
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)
761                 return;
763             _collapseNode(child, shouldCollapse, applyToChildren, true);
764         });
765     };
767     const clear = () => {
768         torrentFilesTable.clear();
769     };
771     return exports();
772 })();
773 Object.freeze(window.qBittorrent.PropFiles);