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.
32 this.current_hash
= "";
34 const normalizePriority = function(priority
) {
36 case FilePriority
.Ignored
:
37 case FilePriority
.Normal
:
38 case FilePriority
.High
:
39 case FilePriority
.Maximum
:
40 case FilePriority
.Mixed
:
43 return FilePriority
.Normal
;
47 const getAllChildren = function(id
, fileId
) {
48 const node
= torrentFilesTable
.getNode(id
);
59 const getChildFiles = function(node
) {
61 node
.children
.each(function(child
) {
66 rowIds
.push(node
.data
.rowId
);
67 fileIds
.push(node
.data
.fileId
);
71 node
.children
.each(function(child
) {
81 const fileCheckboxClicked = function(e
) {
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
);
124 const updateDownloadCheckbox = function(id
, checked
) {
125 const checkbox
= $('cbPrio' + id
);
126 updateCheckbox(checkbox
, checked
);
129 const updateCheckbox = function(checkbox
, checked
) {
131 case TriState
.Checked
:
132 setCheckboxChecked(checkbox
);
134 case TriState
.Unchecked
:
135 setCheckboxUnchecked(checkbox
);
137 case TriState
.Partial
:
138 setCheckboxPartial(checkbox
);
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
);
152 elem
.setAttribute('selected', '');
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
);
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', '');
195 option
.removeAttribute('selected');
198 combobox
.value
= priority
;
201 const switchCheckboxState = function(e
) {
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
) {
219 fileIds
.push(fileId
);
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
) {
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
);
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
)
279 const isAllCheckboxesUnchecked = function() {
280 const checkboxes
= $$('input.DownloadedCB');
281 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
282 if (checkboxes
[i
].checked
)
288 const setFilePriority = function(ids
, fileIds
, priority
) {
289 if (current_hash
=== "") return;
291 clearTimeout(loadTorrentFilesDataTimer
);
293 url
: 'api/v2/torrents/filePrio',
296 'hash': current_hash
,
297 'id': fileIds
.join('|'),
300 onComplete: function() {
301 loadTorrentFilesDataTimer
= loadTorrentFilesData
.delay(1000);
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
324 const new_hash
= torrentsTable
.getCurrentTorrentHash();
325 if (new_hash
=== "") {
326 torrentFilesTable
.clear();
327 clearTimeout(loadTorrentFilesDataTimer
);
328 loadTorrentFilesDataTimer
= loadTorrentFilesData
.delay(5000);
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
);
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();
353 handleNewTorrentFiles(files
);
354 if (loadedNewTorrent
)
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))
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
)));
382 name
: fileName(name
),
385 priority
: normalizePriority(file
.priority
),
386 remaining
: remaining
,
387 availability
: file
.availability
393 addRowsToTable(rows
);
394 updateGlobalCheckbox();
397 const addRowsToTable = function(rows
) {
398 const selectedFiles
= torrentFilesTable
.selectedRowsIds();
401 const rootNode
= new FolderNode();
403 rows
.forEach(function(row
) {
404 let parent
= rootNode
;
405 const pathFolders
= row
.fileName
.split(PathSeparator
);
407 pathFolders
.forEach(function(folderName
) {
408 if (folderName
=== '.unwanted')
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
;
421 if (parentNode
=== null) {
422 parentNode
= new FolderNode();
423 parentNode
.name
= folderName
;
424 parentNode
.rowId
= rowId
;
425 parentNode
.root
= parent
;
426 parent
.addChild(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
);
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");
471 const filesPriorityMenuClicked = function(priority
) {
472 const selectedRows
= torrentFilesTable
.selectedRowsIds();
473 if (selectedRows
.length
=== 0) return;
477 selectedRows
.forEach(function(rowId
) {
478 const elem
= $('comboPrio' + 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',
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
);
522 this.hideItem('FilePrio');
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() === "")
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
574 const rowElem
= span
.parentElement
.parentElement
;
576 rowElem
.addClass("invisible");
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
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];
598 collapseIcon
.addClass("rotate");
600 collapseIcon
.removeClass("rotate");
603 const _isCollapsed = function(node
) {
604 const span
= $('filesTablefileName' + node
.rowId
);
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
) {
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
);
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
)
672 _collapseNode(child
, shouldCollapse
, applyToChildren
, true);