WebUI: Improve torrent deletion
[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>
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.
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
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;
68 const getAllChildren = function(id, fileId) {
69 const node = torrentFilesTable.getNode(id);
70 if (!node.isFolder) {
71 return {
72 rowIds: [id],
73 fileIds: [fileId]
77 const rowIds = [];
78 const fileIds = [];
80 const getChildFiles = function(node) {
81 if (node.isFolder) {
82 node.children.each((child) => {
83 getChildFiles(child);
84 });
86 else {
87 rowIds.push(node.data.rowId);
88 fileIds.push(node.data.fileId);
92 node.children.each((child) => {
93 getChildFiles(child);
94 });
96 return {
97 rowIds: rowIds,
98 fileIds: fileIds
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();
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);
142 return checkbox;
145 const updateDownloadCheckbox = function(id, checked) {
146 const checkbox = $("cbPrio" + id);
147 updateCheckbox(checkbox, checked);
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;
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;
174 return option;
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;
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;
209 else
210 option.selected = false;
213 combobox.value = priority;
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);
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);
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);
264 else
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)
289 return false;
291 return true;
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;
300 return true;
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
318 onComplete: function() {
319 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
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);
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
340 return;
342 const new_hash = torrentsTable.getCurrentTorrentID();
343 if (new_hash === "") {
344 torrentFilesTable.clear();
345 clearTimeout(loadTorrentFilesDataTimer);
346 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
347 return;
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);
356 new Request.JSON({
357 url: url,
358 method: "get",
359 noCache: true,
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();
371 else {
372 handleNewTorrentFiles(files);
373 if (loadedNewTorrent)
374 collapseAllNodes();
377 }).send();
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))
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
409 return row;
412 addRowsToTable(rows);
413 updateGlobalCheckbox();
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;
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;
455 parent = 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);
474 ++rowId;
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");
489 if (isCollapsed)
490 expandNode(node);
491 else
492 collapseNode(node);
495 const expandFolder = function(id) {
496 const node = torrentFilesTable.getNode(id);
497 if (node.isFolder)
498 expandNode(node);
501 const collapseFolder = function(id) {
502 const node = torrentFilesTable.getNode(id);
503 if (node.isFolder)
504 collapseNode(node);
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"));
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)
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
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] }
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);
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);
610 offsets: {
611 x: -15,
612 y: 2
614 onShow: function() {
615 if (is_seed)
616 this.hideItem("FilePrio");
617 else
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 === "")
651 return;
653 torrentFilesTable.updateTable();
655 if (value.trim() === "")
656 collapseAllNodes();
657 else
658 expandAllNodes();
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
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");
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
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");
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";
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) {
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);
767 return exports();
768 })();
769 Object.freeze(window.qBittorrent.PropFiles);