WebUI: Use Map instead of Mootools Hash in Torrents table
[qBittorrent.git] / src / webui / www / private / scripts / prop-files.js
blob6ae12fc8adab19160fdfa420c70fb7c2b945c06f
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         };
47     };
49     const torrentFilesTable = new window.qBittorrent.DynamicTable.TorrentFilesTable();
50     const FilePriority = window.qBittorrent.FileTree.FilePriority;
51     const TriState = window.qBittorrent.FileTree.TriState;
52     let is_seed = true;
53     let current_hash = "";
55     const normalizePriority = function(priority) {
56         switch (priority) {
57             case FilePriority.Ignored:
58             case FilePriority.Normal:
59             case FilePriority.High:
60             case FilePriority.Maximum:
61             case FilePriority.Mixed:
62                 return priority;
63             default:
64                 return FilePriority.Normal;
65         }
66     };
68     const getAllChildren = function(id, fileId) {
69         const node = torrentFilesTable.getNode(id);
70         if (!node.isFolder) {
71             return {
72                 rowIds: [id],
73                 fileIds: [fileId]
74             };
75         }
77         const rowIds = [];
78         const fileIds = [];
80         const getChildFiles = function(node) {
81             if (node.isFolder) {
82                 node.children.each((child) => {
83                     getChildFiles(child);
84                 });
85             }
86             else {
87                 rowIds.push(node.data.rowId);
88                 fileIds.push(node.data.fileId);
89             }
90         };
92         node.children.each((child) => {
93             getChildFiles(child);
94         });
96         return {
97             rowIds: rowIds,
98             fileIds: fileIds
99         };
100     };
102     const fileCheckboxClicked = function(e) {
103         e.stopPropagation();
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();
114     };
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();
126     };
128     const isDownloadCheckboxExists = function(id) {
129         return ($("cbPrio" + id) !== null);
130     };
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);
142         return checkbox;
143     };
145     const updateDownloadCheckbox = function(id, checked) {
146         const checkbox = $("cbPrio" + id);
147         updateCheckbox(checkbox, checked);
148     };
150     const updateCheckbox = function(checkbox, checked) {
151         switch (checked) {
152             case TriState.Checked:
153                 setCheckboxChecked(checkbox);
154                 break;
155             case TriState.Unchecked:
156                 setCheckboxUnchecked(checkbox);
157                 break;
158             case TriState.Partial:
159                 setCheckboxPartial(checkbox);
160                 break;
161         }
162     };
164     const isPriorityComboExists = function(id) {
165         return ($("comboPrio" + id) !== null);
166     };
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;
174             return option;
175         };
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);
194         return select;
195     };
197     const updatePriorityCombo = function(id, selectedPriority) {
198         const combobox = $("comboPrio" + id);
199         if (parseInt(combobox.value, 10) !== selectedPriority)
200             selectComboboxPriority(combobox, selectedPriority);
201     };
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;
209             else
210                 option.selected = false;
211         }
213         combobox.value = priority;
214     };
216     const switchCheckboxState = function(e) {
217         e.stopPropagation();
219         const rowIds = [];
220         const fileIds = [];
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) {
233                     rowIds.push(rowId);
234                     fileIds.push(fileId);
235                 }
236             });
237         }
238         else {
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) {
248                     rowIds.push(rowId);
249                     fileIds.push(fileId);
250                 }
251             });
252         }
254         if (rowIds.length > 0)
255             setFilePriority(rowIds, fileIds, priority);
256     };
258     const updateGlobalCheckbox = function() {
259         const checkbox = $("tristate_cb");
260         if (isAllCheckboxesChecked())
261             setCheckboxChecked(checkbox);
262         else if (isAllCheckboxesUnchecked())
263             setCheckboxUnchecked(checkbox);
264         else
265             setCheckboxPartial(checkbox);
266     };
268     const setCheckboxChecked = function(checkbox) {
269         checkbox.state = "checked";
270         checkbox.indeterminate = false;
271         checkbox.checked = true;
272     };
274     const setCheckboxUnchecked = function(checkbox) {
275         checkbox.state = "unchecked";
276         checkbox.indeterminate = false;
277         checkbox.checked = false;
278     };
280     const setCheckboxPartial = function(checkbox) {
281         checkbox.state = "partial";
282         checkbox.indeterminate = true;
283     };
285     const isAllCheckboxesChecked = function() {
286         const checkboxes = $$("input.DownloadedCB");
287         for (let i = 0; i < checkboxes.length; ++i) {
288             if (!checkboxes[i].checked)
289                 return false;
290         }
291         return true;
292     };
294     const isAllCheckboxesUnchecked = function() {
295         const checkboxes = $$("input.DownloadedCB");
296         for (let i = 0; i < checkboxes.length; ++i) {
297             if (checkboxes[i].checked)
298                 return false;
299         }
300         return true;
301     };
303     const setFilePriority = function(ids, fileIds, priority) {
304         if (current_hash === "")
305             return;
307         clearTimeout(loadTorrentFilesDataTimer);
308         loadTorrentFilesDataTimer = -1;
310         new Request({
311             url: "api/v2/torrents/filePrio",
312             method: "post",
313             data: {
314                 "hash": current_hash,
315                 "id": fileIds.join("|"),
316                 "priority": priority
317             },
318             onComplete: function() {
319                 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
320             }
321         }).send();
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);
330         });
332         torrentFilesTable.updateTable(false);
333     };
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
340             return;
341         }
342         const new_hash = torrentsTable.getCurrentTorrentID();
343         if (new_hash === "") {
344             torrentFilesTable.clear();
345             clearTimeout(loadTorrentFilesDataTimer);
346             loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
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: function() {
361                 clearTimeout(loadTorrentFilesDataTimer);
362                 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
363             },
364             onSuccess: function(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 = function() {
381         clearTimeout(loadTorrentFilesDataTimer);
382         loadTorrentFilesDataTimer = -1;
383         loadTorrentFilesData();
384     };
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))
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 = function(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 = function(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 = function(id) {
496         const node = torrentFilesTable.getNode(id);
497         if (node.isFolder)
498             expandNode(node);
499     };
501     const collapseFolder = function(id) {
502         const node = torrentFilesTable.getNode(id);
503         if (node.isFolder)
504             collapseNode(node);
505     };
507     const filesPriorityMenuClicked = function(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 = function(hash) {
536         const rowId = torrentFilesTable.selectedRowsIds()[0];
537         if (rowId === undefined)
538             return;
539         const row = torrentFilesTable.rows[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 = function(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: function(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: function(element, ref) {
598                 filesPriorityMenuClicked(FilePriority.Ignored);
599             },
600             FilePrioNormal: function(element, ref) {
601                 filesPriorityMenuClicked(FilePriority.Normal);
602             },
603             FilePrioHigh: function(element, ref) {
604                 filesPriorityMenuClicked(FilePriority.High);
605             },
606             FilePrioMaximum: function(element, ref) {
607                 filesPriorityMenuClicked(FilePriority.Maximum);
608             }
609         },
610         offsets: {
611             x: -15,
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 = function(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 = function(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 = function(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 = function(node) {
708         _collapseNode(node, false, false, false);
709     };
711     const collapseNode = function(node) {
712         _collapseNode(node, true, false, false);
713     };
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);
720             });
721         });
722     };
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);
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 = function(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     return exports();
768 })();
769 Object.freeze(window.qBittorrent.PropFiles);