Rename WebUI content files
[qBittorrent.git] / src / webui / www / private / scripts / prop-files.js
blob485b54f2a81d0ed0104f3fd94bb8831036ea9700
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 let is_seed = true;
32 this.current_hash = "";
34 const normalizePriority = function(priority) {
35 switch (priority) {
36 case FilePriority.Ignored:
37 case FilePriority.Normal:
38 case FilePriority.High:
39 case FilePriority.Maximum:
40 case FilePriority.Mixed:
41 return priority;
42 default:
43 return FilePriority.Normal;
47 const getAllChildren = function(id, fileId) {
48 const node = torrentFilesTable.getNode(id);
49 if (!node.isFolder) {
50 return {
51 rowIds: [id],
52 fileIds: [fileId]
56 const rowIds = [];
57 const fileIds = [];
59 const getChildFiles = function(node) {
60 if (node.isFolder) {
61 node.children.each(function(child) {
62 getChildFiles(child);
63 });
65 else {
66 rowIds.push(node.data.rowId);
67 fileIds.push(node.data.fileId);
71 node.children.each(function(child) {
72 getChildFiles(child);
73 });
75 return {
76 rowIds: rowIds,
77 fileIds: fileIds
81 const fileCheckboxClicked = function(e) {
82 e.stopPropagation();
84 const checkbox = e.target;
85 const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored;
86 const id = checkbox.get('data-id');
87 const fileId = checkbox.get('data-file-id');
89 const rows = getAllChildren(id, fileId);
91 setFilePriority(rows.rowIds, rows.fileIds, priority);
92 updateGlobalCheckbox();
95 const fileComboboxChanged = function(e) {
96 const combobox = e.target;
97 const priority = combobox.value;
98 const id = combobox.get('data-id');
99 const fileId = combobox.get('data-file-id');
101 const rows = getAllChildren(id, fileId);
103 setFilePriority(rows.rowIds, rows.fileIds, priority);
104 updateGlobalCheckbox();
107 const isDownloadCheckboxExists = function(id) {
108 return ($('cbPrio' + id) !== null);
111 const createDownloadCheckbox = function(id, fileId, checked) {
112 const checkbox = new Element('input');
113 checkbox.set('type', 'checkbox');
114 checkbox.set('id', 'cbPrio' + id);
115 checkbox.set('data-id', id);
116 checkbox.set('data-file-id', fileId);
117 checkbox.set('class', 'DownloadedCB');
118 checkbox.addEvent('click', fileCheckboxClicked);
120 updateCheckbox(checkbox, checked);
121 return checkbox;
124 const updateDownloadCheckbox = function(id, checked) {
125 const checkbox = $('cbPrio' + id);
126 updateCheckbox(checkbox, checked);
129 const updateCheckbox = function(checkbox, checked) {
130 switch (checked) {
131 case TriState.Checked:
132 setCheckboxChecked(checkbox);
133 break;
134 case TriState.Unchecked:
135 setCheckboxUnchecked(checkbox);
136 break;
137 case TriState.Partial:
138 setCheckboxPartial(checkbox);
139 break;
143 const isPriorityComboExists = function(id) {
144 return ($('comboPrio' + id) !== null);
147 const createPriorityOptionElement = function(priority, selected, html) {
148 const elem = new Element('option');
149 elem.set('value', priority.toString());
150 elem.set('html', html);
151 if (selected)
152 elem.setAttribute('selected', '');
153 return elem;
156 const createPriorityCombo = function(id, fileId, selectedPriority) {
157 const select = new Element('select');
158 select.set('id', 'comboPrio' + id);
159 select.set('data-id', id);
160 select.set('data-file-id', fileId);
161 select.set('disabled', is_seed);
162 select.addClass('combo_priority');
163 select.addEvent('change', fileComboboxChanged);
165 createPriorityOptionElement(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), 'QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select);
166 createPriorityOptionElement(FilePriority.Normal, (FilePriority.Normal === selectedPriority), 'QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select);
167 createPriorityOptionElement(FilePriority.High, (FilePriority.High === selectedPriority), 'QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select);
168 createPriorityOptionElement(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), 'QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select);
170 // "Mixed" priority is for display only; it shouldn't be selectable
171 const mixedPriorityOption = createPriorityOptionElement(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), 'QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]');
172 mixedPriorityOption.set('disabled', true);
173 mixedPriorityOption.injectInside(select);
175 return select;
178 const updatePriorityCombo = function(id, selectedPriority) {
179 const combobox = $('comboPrio' + id);
181 if (parseInt(combobox.value) !== selectedPriority)
182 selectComboboxPriority(combobox, selectedPriority);
184 if (combobox.disabled !== is_seed)
185 combobox.disabled = is_seed;
188 const selectComboboxPriority = function(combobox, priority) {
189 const options = combobox.options;
190 for (let i = 0; i < options.length; ++i) {
191 const option = options[i];
192 if (parseInt(option.value) === priority)
193 option.setAttribute('selected', '');
194 else
195 option.removeAttribute('selected');
198 combobox.value = priority;
201 const switchCheckboxState = function(e) {
202 e.stopPropagation();
204 const rowIds = [];
205 const fileIds = [];
206 let priority = FilePriority.Ignored;
207 const checkbox = $('tristate_cb');
209 if (checkbox.state === "checked") {
210 setCheckboxUnchecked(checkbox);
211 // set file priority for all checked to Ignored
212 torrentFilesTable.getFilteredAndSortedRows().forEach(function(row) {
213 const rowId = row.rowId;
214 const fileId = row.full_data.fileId;
215 const isChecked = (row.full_data.checked === TriState.Checked);
216 const isFolder = (fileId === -1);
217 if (!isFolder && isChecked) {
218 rowIds.push(rowId);
219 fileIds.push(fileId);
223 else {
224 setCheckboxChecked(checkbox);
225 priority = FilePriority.Normal;
226 // set file priority for all unchecked to Normal
227 torrentFilesTable.getFilteredAndSortedRows().forEach(function(row) {
228 const rowId = row.rowId;
229 const fileId = row.full_data.fileId;
230 const isUnchecked = (row.full_data.checked === TriState.Unchecked);
231 const isFolder = (fileId === -1);
232 if (!isFolder && isUnchecked) {
233 rowIds.push(rowId);
234 fileIds.push(fileId);
239 if (rowIds.length > 0)
240 setFilePriority(rowIds, fileIds, priority);
243 const updateGlobalCheckbox = function() {
244 const checkbox = $('tristate_cb');
245 if (isAllCheckboxesChecked())
246 setCheckboxChecked(checkbox);
247 else if (isAllCheckboxesUnchecked())
248 setCheckboxUnchecked(checkbox);
249 else
250 setCheckboxPartial(checkbox);
253 const setCheckboxChecked = function(checkbox) {
254 checkbox.state = "checked";
255 checkbox.indeterminate = false;
256 checkbox.checked = true;
259 const setCheckboxUnchecked = function(checkbox) {
260 checkbox.state = "unchecked";
261 checkbox.indeterminate = false;
262 checkbox.checked = false;
265 const setCheckboxPartial = function(checkbox) {
266 checkbox.state = "partial";
267 checkbox.indeterminate = true;
270 const isAllCheckboxesChecked = function() {
271 const checkboxes = $$('input.DownloadedCB');
272 for (let i = 0; i < checkboxes.length; ++i) {
273 if (!checkboxes[i].checked)
274 return false;
276 return true;
279 const isAllCheckboxesUnchecked = function() {
280 const checkboxes = $$('input.DownloadedCB');
281 for (let i = 0; i < checkboxes.length; ++i) {
282 if (checkboxes[i].checked)
283 return false;
285 return true;
288 const setFilePriority = function(ids, fileIds, priority) {
289 if (current_hash === "") return;
291 clearTimeout(loadTorrentFilesDataTimer);
292 new Request({
293 url: 'api/v2/torrents/filePrio',
294 method: 'post',
295 data: {
296 'hash': current_hash,
297 'id': fileIds.join('|'),
298 'priority': priority
300 onComplete: function() {
301 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
303 }).send();
305 const ignore = (priority === FilePriority.Ignored);
306 ids.forEach(function(_id) {
307 torrentFilesTable.setIgnored(_id, ignore);
309 const combobox = $('comboPrio' + _id);
310 if (combobox !== null)
311 selectComboboxPriority(combobox, priority);
314 torrentFilesTable.updateTable(false);
317 let loadTorrentFilesDataTimer;
318 const loadTorrentFilesData = function() {
319 if ($('prop_files').hasClass('invisible')
320 || $('propertiesPanel_collapseToggle').hasClass('panel-expand')) {
321 // Tab changed, don't do anything
322 return;
324 const new_hash = torrentsTable.getCurrentTorrentHash();
325 if (new_hash === "") {
326 torrentFilesTable.clear();
327 clearTimeout(loadTorrentFilesDataTimer);
328 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
329 return;
331 let loadedNewTorrent = false;
332 if (new_hash != current_hash) {
333 torrentFilesTable.clear();
334 current_hash = new_hash;
335 loadedNewTorrent = true;
337 const url = new URI('api/v2/torrents/files?hash=' + current_hash);
338 new Request.JSON({
339 url: url,
340 noCache: true,
341 method: 'get',
342 onComplete: function() {
343 clearTimeout(loadTorrentFilesDataTimer);
344 loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000);
346 onSuccess: function(files) {
347 clearTimeout(torrentFilesFilterInputTimer);
349 if (files.length === 0) {
350 torrentFilesTable.clear();
352 else {
353 handleNewTorrentFiles(files);
354 if (loadedNewTorrent)
355 collapseAllNodes();
358 }).send();
361 updateTorrentFilesData = function() {
362 clearTimeout(loadTorrentFilesDataTimer);
363 loadTorrentFilesData();
366 const handleNewTorrentFiles = function(files) {
367 is_seed = (files.length > 0) ? files[0].is_seed : true;
369 const rows = files.map(function(file, index) {
370 let progress = (file.progress * 100).round(1);
371 if ((progress === 100) && (file.progress < 1))
372 progress = 99.9;
374 const name = escapeHtml(file.name);
375 const ignore = (file.priority === FilePriority.Ignored);
376 const checked = (ignore ? TriState.Unchecked : TriState.Checked);
377 const remaining = (ignore ? 0 : (file.size * (1.0 - file.progress)));
378 const row = {
379 fileId: index,
380 checked: checked,
381 fileName: name,
382 name: fileName(name),
383 size: file.size,
384 progress: progress,
385 priority: normalizePriority(file.priority),
386 remaining: remaining,
387 availability: file.availability
390 return row;
393 addRowsToTable(rows);
394 updateGlobalCheckbox();
397 const addRowsToTable = function(rows) {
398 const selectedFiles = torrentFilesTable.selectedRowsIds();
399 let rowId = 0;
401 const rootNode = new FolderNode();
403 rows.forEach(function(row) {
404 let parent = rootNode;
405 const pathFolders = row.fileName.split(PathSeparator);
406 pathFolders.pop();
407 pathFolders.forEach(function(folderName) {
408 if (folderName === '.unwanted')
409 return;
411 let parentNode = null;
412 if (parent.children !== null) {
413 for (let i = 0; i < parent.children.length; ++i) {
414 const childFolder = parent.children[i];
415 if (childFolder.name === folderName) {
416 parentNode = childFolder;
417 break;
421 if (parentNode === null) {
422 parentNode = new FolderNode();
423 parentNode.name = folderName;
424 parentNode.rowId = rowId;
425 parentNode.root = parent;
426 parent.addChild(parentNode);
428 ++rowId;
431 parent = parentNode;
434 const isChecked = row.checked ? TriState.Checked : TriState.Unchecked;
435 const remaining = (row.priority === FilePriority.Ignored) ? 0 : row.remaining;
436 const childNode = new FileNode();
437 childNode.name = row.name;
438 childNode.rowId = rowId;
439 childNode.size = row.size;
440 childNode.checked = isChecked;
441 childNode.remaining = remaining;
442 childNode.progress = row.progress;
443 childNode.priority = row.priority;
444 childNode.availability = row.availability;
445 childNode.root = parent;
446 childNode.data = row;
447 parent.addChild(childNode);
449 ++rowId;
450 }.bind(this));
452 torrentFilesTable.populateTable(rootNode);
453 torrentFilesTable.updateTable(false);
454 torrentFilesTable.altRow();
456 if (selectedFiles.length > 0)
457 torrentFilesTable.reselectRows(selectedFiles);
460 const collapseIconClicked = function(event) {
461 const id = event.get("data-id");
462 const node = torrentFilesTable.getNode(id);
463 const isCollapsed = (event.parentElement.get("data-collapsed") === "true");
465 if (isCollapsed)
466 expandNode(node);
467 else
468 collapseNode(node);
471 const filesPriorityMenuClicked = function(priority) {
472 const selectedRows = torrentFilesTable.selectedRowsIds();
473 if (selectedRows.length === 0) return;
475 const rowIds = [];
476 const fileIds = [];
477 selectedRows.forEach(function(rowId) {
478 const elem = $('comboPrio' + rowId);
479 rowIds.push(rowId);
480 fileIds.push(elem.get("data-file-id"));
483 const uniqueRowIds = {};
484 const uniqueFileIds = {};
485 for (let i = 0; i < rowIds.length; ++i) {
486 const rows = getAllChildren(rowIds[i], fileIds[i]);
487 rows.rowIds.forEach(function(rowId) {
488 uniqueRowIds[rowId] = true;
490 rows.fileIds.forEach(function(fileId) {
491 uniqueFileIds[fileId] = true;
495 setFilePriority(Object.keys(uniqueRowIds), Object.keys(uniqueFileIds), priority);
498 const torrentFilesContextMenu = new ContextMenu({
499 targets: '#torrentFilesTableDiv tr',
500 menu: 'torrentFilesMenu',
501 actions: {
503 FilePrioIgnore: function(element, ref) {
504 filesPriorityMenuClicked(FilePriority.Ignored);
506 FilePrioNormal: function(element, ref) {
507 filesPriorityMenuClicked(FilePriority.Normal);
509 FilePrioHigh: function(element, ref) {
510 filesPriorityMenuClicked(FilePriority.High);
512 FilePrioMaximum: function(element, ref) {
513 filesPriorityMenuClicked(FilePriority.Maximum);
516 offsets: {
517 x: -15,
518 y: 2
520 onShow: function() {
521 if (is_seed)
522 this.hideItem('FilePrio');
523 else
524 this.showItem('FilePrio');
528 torrentFilesTable.setup('torrentFilesTableDiv', 'torrentFilesTableFixedHeaderDiv', torrentFilesContextMenu);
529 // inject checkbox into table header
530 const tableHeaders = $$('#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th');
531 if (tableHeaders.length > 0) {
532 const checkbox = new Element('input');
533 checkbox.set('type', 'checkbox');
534 checkbox.set('id', 'tristate_cb');
535 checkbox.addEvent('click', switchCheckboxState);
537 const checkboxTH = tableHeaders[0];
538 checkbox.injectInside(checkboxTH);
541 // default sort by name column
542 if (torrentFilesTable.getSortedColumn() === null)
543 torrentFilesTable.setSortedColumn('name');
545 let prevTorrentFilesFilterValue;
546 let torrentFilesFilterInputTimer = null;
547 // listen for changes to torrentFilesFilterInput
548 $('torrentFilesFilterInput').addEvent('input', function() {
549 const value = $('torrentFilesFilterInput').get("value");
550 if (value !== prevTorrentFilesFilterValue) {
551 prevTorrentFilesFilterValue = value;
552 torrentFilesTable.setFilter(value);
553 clearTimeout(torrentFilesFilterInputTimer);
554 torrentFilesFilterInputTimer = setTimeout(function() {
555 if (current_hash === "") return;
556 torrentFilesTable.updateTable(false);
558 if (value.trim() === "")
559 collapseAllNodes();
560 else
561 expandAllNodes();
562 }, 400);
567 * Show/hide a node's row
569 const _hideNode = function(node, shouldHide) {
570 const span = $('filesTablefileName' + node.rowId);
571 // span won't exist if row has been filtered out
572 if (span === null)
573 return;
574 const rowElem = span.parentElement.parentElement;
575 if (shouldHide)
576 rowElem.addClass("invisible");
577 else
578 rowElem.removeClass("invisible");
582 * Update a node's collapsed state and icon
584 const _updateNodeState = function(node, isCollapsed) {
585 const span = $('filesTablefileName' + node.rowId);
586 // span won't exist if row has been filtered out
587 if (span === null)
588 return;
589 const td = span.parentElement;
590 const rowElem = td.parentElement;
592 // store collapsed state
593 td.set("data-collapsed", isCollapsed);
595 // rotate the collapse icon
596 const collapseIcon = td.getElementsByClassName("filesTableCollapseIcon")[0];
597 if (isCollapsed)
598 collapseIcon.addClass("rotate");
599 else
600 collapseIcon.removeClass("rotate");
603 const _isCollapsed = function(node) {
604 const span = $('filesTablefileName' + node.rowId);
605 if (span === null)
606 return true;
608 const td = span.parentElement;
609 return (td.get("data-collapsed") === "true");
612 const expandNode = function(node) {
613 _collapseNode(node, false, false, false);
614 torrentFilesTable.altRow();
617 const collapseNode = function(node) {
618 _collapseNode(node, true, false, false);
619 torrentFilesTable.altRow();
622 const expandAllNodes = function() {
623 const root = torrentFilesTable.getRoot();
624 root.children.each(function(node) {
625 node.children.each(function(child) {
626 _collapseNode(child, false, true, false);
629 torrentFilesTable.altRow();
632 const collapseAllNodes = function() {
633 const root = torrentFilesTable.getRoot();
634 root.children.each(function(node) {
635 node.children.each(function(child) {
636 _collapseNode(child, true, true, false);
639 torrentFilesTable.altRow();
643 * Collapses a folder node with the option to recursively collapse all children
644 * @param {FolderNode} node the node to collapse/expand
645 * @param {boolean} shouldCollapse true if the node should be collapsed, false if it should be expanded
646 * @param {boolean} applyToChildren true if the node's children should also be collapsed, recursively
647 * @param {boolean} isChildNode true if the current node is a child of the original node we collapsed/expanded
649 const _collapseNode = function(node, shouldCollapse, applyToChildren, isChildNode) {
650 if (!node.isFolder)
651 return;
653 const shouldExpand = !shouldCollapse;
654 const isNodeCollapsed = _isCollapsed(node);
655 const nodeInCorrectState = ((shouldCollapse && isNodeCollapsed) || (shouldExpand && !isNodeCollapsed));
656 const canSkipNode = (isChildNode && (!applyToChildren || nodeInCorrectState));
657 if (!isChildNode || applyToChildren || !canSkipNode)
658 _updateNodeState(node, shouldCollapse);
660 node.children.each(function(child) {
661 _hideNode(child, shouldCollapse);
663 if (!child.isFolder)
664 return;
666 // don't expand children that have been independently collapsed, unless applyToChildren is true
667 const shouldExpandChildren = (shouldExpand && applyToChildren);
668 const isChildCollapsed = _isCollapsed(child);
669 if (!shouldExpandChildren && isChildCollapsed)
670 return;
672 _collapseNode(child, shouldCollapse, applyToChildren, true);