Allow WebUI Content tab to be sorted
[qBittorrent.git] / src / webui / www / private / scripts / dynamicTable.js
bloba74e7b308030f2896cfe474812e9070b9dbd42c7
1 /*
2  * MIT License
3  * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org> & Christophe Dumez <chris@qbittorrent.org>
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a copy
6  * of this software and associated documentation files (the "Software"), to deal
7  * in the Software without restriction, including without limitation the rights
8  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9  * copies of the Software, and to permit persons to whom the Software is
10  * furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included in
13  * all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21  * THE SOFTWARE.
22  */
24 /**************************************************************
26     Script      : Dynamic Table
27     Version     : 0.5
28     Authors     : Ishan Arora & Christophe Dumez
29     Desc        : Programmable sortable table
30     Licence     : Open Source MIT Licence
32  **************************************************************/
34 'use strict';
36 var DynamicTableHeaderContextMenuClass = null;
37 var ProgressColumnWidth = -1;
39 var DynamicTable = new Class({
41     initialize: function() {},
43     setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
44         this.dynamicTableDivId = dynamicTableDivId;
45         this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
46         this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements('tr')[0];
47         this.hiddenTableHeader = $(dynamicTableDivId).getElements('tr')[0];
48         this.tableBody = $(dynamicTableDivId).getElements('tbody')[0];
49         this.rows = new Hash();
50         this.selectedRows = [];
51         this.columns = [];
52         this.contextMenu = contextMenu;
53         this.sortedColumn = getLocalStorageItem('sorted_column_' + this.dynamicTableDivId, 0);
54         this.reverseSort = getLocalStorageItem('reverse_sort_' + this.dynamicTableDivId, '0');
55         this.initColumns();
56         this.loadColumnsOrder();
57         this.updateTableHeaders();
58         this.setupCommonEvents();
59         this.setupHeaderEvents();
60         this.setupHeaderMenu();
61         this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === '1'));
62     },
64     setupCommonEvents: function() {
65         var scrollFn = function() {
66             $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
67         }.bind(this);
69         $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
71         // if the table exists within a panel
72         if ($(this.dynamicTableDivId).getParent('.panel')) {
73             var resizeFn = function() {
74                 var panel = $(this.dynamicTableDivId).getParent('.panel');
75                 var h = panel.getBoundingClientRect().height - $(this.dynamicTableFixedHeaderDivId).getBoundingClientRect().height;
76                 $(this.dynamicTableDivId).style.height = h + 'px';
78                 // Workaround due to inaccurate calculation of elements heights by browser
80                 var n = 2;
82                 while (panel.clientWidth != panel.offsetWidth && n > 0) { // is panel vertical scrollbar visible ?
83                     --n;
84                     h -= 0.5;
85                     $(this.dynamicTableDivId).style.height = h + 'px';
86                 }
88                 this.lastPanelHeight = panel.getBoundingClientRect().height;
89             }.bind(this);
91             $(this.dynamicTableDivId).getParent('.panel').addEvent('resize', resizeFn);
93             this.lastPanelHeight = 0;
95             // Workaround. Resize event is called not always (for example it isn't called when browser window changes it's size)
97             var checkResizeFn = function() {
98                 var panel = $(this.dynamicTableDivId).getParent('.panel');
99                 if (this.lastPanelHeight != panel.getBoundingClientRect().height) {
100                     this.lastPanelHeight = panel.getBoundingClientRect().height;
101                     panel.fireEvent('resize');
102                 }
103             }.bind(this);
105             setInterval(checkResizeFn, 500);
106         }
107     },
109     setupHeaderEvents: function() {
110         this.currentHeaderAction = '';
111         this.canResize = false;
113         var resetElementBorderStyle = function(el, side) {
114             if (side === 'left' || side !== 'right') {
115                 el.setStyle('border-left-style', '');
116                 el.setStyle('border-left-color', '');
117                 el.setStyle('border-left-width', '');
118             }
119             if (side === 'right' || side !== 'left') {
120                 el.setStyle('border-right-style', '');
121                 el.setStyle('border-right-color', '');
122                 el.setStyle('border-right-width', '');
123             }
124         };
126         var mouseMoveFn = function(e) {
127             var brect = e.target.getBoundingClientRect();
128             var mouseXRelative = e.event.clientX - brect.left;
129             if (this.currentHeaderAction === '') {
130                 if (brect.width - mouseXRelative < 5) {
131                     this.resizeTh = e.target;
132                     this.canResize = true;
133                     e.target.getParent("tr").style.cursor = 'col-resize';
134                 }
135                 else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) {
136                     this.resizeTh = e.target.getPrevious('[class=""]');
137                     this.canResize = true;
138                     e.target.getParent("tr").style.cursor = 'col-resize';
139                 }
140                 else {
141                     this.canResize = false;
142                     e.target.getParent("tr").style.cursor = '';
143                 }
144             }
145             if (this.currentHeaderAction === 'drag') {
146                 var previousVisibleSibling = e.target.getPrevious('[class=""]');
147                 var borderChangeElement = previousVisibleSibling;
148                 var changeBorderSide = 'right';
150                 if (mouseXRelative > brect.width / 2) {
151                     borderChangeElement = e.target;
152                     this.dropSide = 'right';
153                 }
154                 else {
155                     this.dropSide = 'left';
156                 }
158                 e.target.getParent("tr").style.cursor = 'move';
160                 if (!previousVisibleSibling) { // right most column
161                     borderChangeElement = e.target;
163                     if (mouseXRelative <= brect.width / 2)
164                         changeBorderSide = 'left';
165                 }
167                 borderChangeElement.setStyle('border-' + changeBorderSide + '-style', 'solid');
168                 borderChangeElement.setStyle('border-' + changeBorderSide + '-color', '#e60');
169                 borderChangeElement.setStyle('border-' + changeBorderSide + '-width', 'initial');
171                 resetElementBorderStyle(borderChangeElement, changeBorderSide === 'right' ? 'left' : 'right');
173                 borderChangeElement.getSiblings('[class=""]').each(function(el) {
174                     resetElementBorderStyle(el);
175                 });
176             }
177             this.lastHoverTh = e.target;
178             this.lastClientX = e.event.clientX;
179         }.bind(this);
181         var mouseOutFn = function(e) {
182             resetElementBorderStyle(e.target);
183         }.bind(this);
185         var onBeforeStart = function(el) {
186             this.clickedTh = el;
187             this.currentHeaderAction = 'start';
188             this.dragMovement = false;
189             this.dragStartX = this.lastClientX;
190         }.bind(this);
192         var onStart = function(el, event) {
193             if (this.canResize) {
194                 this.currentHeaderAction = 'resize';
195                 this.startWidth = this.resizeTh.getStyle('width').toFloat();
196             }
197             else {
198                 this.currentHeaderAction = 'drag';
199                 el.setStyle('background-color', '#C1D5E7');
200             }
201         }.bind(this);
203         var onDrag = function(el, event) {
204             if (this.currentHeaderAction === 'resize') {
205                 var width = this.startWidth + (event.page.x - this.dragStartX);
206                 if (width < 16)
207                     width = 16;
208                 this.columns[this.resizeTh.columnName].width = width;
209                 this.updateColumn(this.resizeTh.columnName);
210             }
211         }.bind(this);
213         var onComplete = function(el, event) {
214             resetElementBorderStyle(this.lastHoverTh);
215             el.setStyle('background-color', '');
216             if (this.currentHeaderAction === 'resize')
217                 localStorage.setItem('column_' + this.resizeTh.columnName + '_width_' + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width);
218             if ((this.currentHeaderAction === 'drag') && (el !== this.lastHoverTh)) {
219                 this.saveColumnsOrder();
220                 var val = localStorage.getItem('columns_order_' + this.dynamicTableDivId).split(',');
221                 val.erase(el.columnName);
222                 var pos = val.indexOf(this.lastHoverTh.columnName);
223                 if (this.dropSide === 'right') ++pos;
224                 val.splice(pos, 0, el.columnName);
225                 localStorage.setItem('columns_order_' + this.dynamicTableDivId, val.join(','));
226                 this.loadColumnsOrder();
227                 this.updateTableHeaders();
228                 while (this.tableBody.firstChild)
229                     this.tableBody.removeChild(this.tableBody.firstChild);
230                 this.updateTable(true);
231             }
232             if (this.currentHeaderAction === 'drag') {
233                 resetElementBorderStyle(el);
234                 el.getSiblings('[class=""]').each(function(el) {
235                     resetElementBorderStyle(el);
236                 });
237             }
238             this.currentHeaderAction = '';
239         }.bind(this);
241         var onCancel = function(el) {
242             this.currentHeaderAction = '';
243             this.setSortedColumn(el.columnName);
244         }.bind(this);
246         var ths = this.fixedTableHeader.getElements('th');
248         for (var i = 0; i < ths.length; ++i) {
249             var th = ths[i];
250             th.addEvent('mousemove', mouseMoveFn);
251             th.addEvent('mouseout', mouseOutFn);
252             th.makeResizable({
253                 modifiers: {
254                     x: '',
255                     y: ''
256                 },
257                 onBeforeStart: onBeforeStart,
258                 onStart: onStart,
259                 onDrag: onDrag,
260                 onComplete: onComplete,
261                 onCancel: onCancel
262             });
263         }
264     },
266     setupDynamicTableHeaderContextMenuClass: function() {
267         if (!DynamicTableHeaderContextMenuClass) {
268             DynamicTableHeaderContextMenuClass = new Class({
269                 Extends: ContextMenu,
270                 updateMenuItems: function() {
271                     for (var i = 0; i < this.dynamicTable.columns.length; ++i) {
272                         if (this.dynamicTable.columns[i].caption === '')
273                             continue;
274                         if (this.dynamicTable.columns[i].visible !== '0')
275                             this.setItemChecked(this.dynamicTable.columns[i].name, true);
276                         else
277                             this.setItemChecked(this.dynamicTable.columns[i].name, false);
278                     }
279                 }
280             });
281         }
282     },
284     showColumn: function(columnName, show) {
285         this.columns[columnName].visible = show ? '1' : '0';
286         localStorage.setItem('column_' + columnName + '_visible_' + this.dynamicTableDivId, show ? '1' : '0');
287         this.updateColumn(columnName);
288     },
290     setupHeaderMenu: function() {
291         this.setupDynamicTableHeaderContextMenuClass();
293         var menuId = this.dynamicTableDivId + '_headerMenu';
295         var ul = new Element('ul', {
296             id: menuId,
297             class: 'contextMenu scrollableMenu'
298         });
300         var createLi = function(columnName, text) {
301             var html = '<a href="#' + columnName + '" ><img src="images/qbt-theme/checked.svg"/>' + escapeHtml(text) + '</a>';
302             return new Element('li', {
303                 html: html
304             });
305         };
307         var actions = {};
309         var onMenuItemClicked = function(element, ref, action) {
310             this.showColumn(action, this.columns[action].visible === '0');
311         }.bind(this);
313         for (var i = 0; i < this.columns.length; ++i) {
314             var text = this.columns[i].caption;
315             if (text === '')
316                 continue;
317             ul.appendChild(createLi(this.columns[i].name, text));
318             actions[this.columns[i].name] = onMenuItemClicked;
319         }
321         ul.inject(document.body);
323         this.headerContextMenu = new DynamicTableHeaderContextMenuClass({
324             targets: '#' + this.dynamicTableFixedHeaderDivId + ' tr',
325             actions: actions,
326             menu: menuId,
327             offsets: {
328                 x: -15,
329                 y: 2
330             }
331         });
333         this.headerContextMenu.dynamicTable = this;
334     },
336     initColumns: function() {},
338     newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
339         var column = {};
340         column['name'] = name;
341         column['visible'] = getLocalStorageItem('column_' + name + '_visible_' + this.dynamicTableDivId, defaultVisible ? '1' : '0');
342         column['force_hide'] = false;
343         column['caption'] = caption;
344         column['style'] = style;
345         column['width'] = getLocalStorageItem('column_' + name + '_width_' + this.dynamicTableDivId, defaultWidth);
346         column['dataProperties'] = [name];
347         column['getRowValue'] = function(row, pos) {
348             if (pos === undefined)
349                 pos = 0;
350             return row['full_data'][this.dataProperties[pos]];
351         };
352         column['compareRows'] = function(row1, row2) {
353             if (this.getRowValue(row1) < this.getRowValue(row2))
354                 return -1;
355             else if (this.getRowValue(row1) > this.getRowValue(row2))
356                 return 1;
357             else return 0;
358         };
359         column['updateTd'] = function(td, row) {
360             td.innerHTML = this.getRowValue(row);
361         };
362         column['onResize'] = null;
363         this.columns.push(column);
364         this.columns[name] = column;
366         this.hiddenTableHeader.appendChild(new Element('th'));
367         this.fixedTableHeader.appendChild(new Element('th'));
368     },
370     loadColumnsOrder: function() {
371         var columnsOrder = [];
372         var val = localStorage.getItem('columns_order_' + this.dynamicTableDivId);
373         if (val === null || val === undefined) return;
374         val.split(',').forEach(function(v) {
375             if ((v in this.columns) && (!columnsOrder.contains(v)))
376                 columnsOrder.push(v);
377         }.bind(this));
379         for (var i = 0; i < this.columns.length; ++i)
380             if (!columnsOrder.contains(this.columns[i].name))
381                 columnsOrder.push(this.columns[i].name);
383         for (i = 0; i < this.columns.length; ++i)
384             this.columns[i] = this.columns[columnsOrder[i]];
385     },
387     saveColumnsOrder: function() {
388         var val = '';
389         for (var i = 0; i < this.columns.length; ++i) {
390             if (i > 0)
391                 val += ',';
392             val += this.columns[i].name;
393         }
394         localStorage.setItem('columns_order_' + this.dynamicTableDivId, val);
395     },
397     updateTableHeaders: function() {
398         this.updateHeader(this.hiddenTableHeader);
399         this.updateHeader(this.fixedTableHeader);
400     },
402     updateHeader: function(header) {
403         var ths = header.getElements('th');
405         for (var i = 0; i < ths.length; ++i) {
406             var th = ths[i];
407             th._this = this;
408             th.setAttribute('title', this.columns[i].caption);
409             th.innerHTML = this.columns[i].caption;
410             th.setAttribute('style', 'width: ' + this.columns[i].width + 'px;' + this.columns[i].style);
411             th.columnName = this.columns[i].name;
412             th.addClass('column_' + th.columnName);
413             if ((this.columns[i].visible == '0') || this.columns[i].force_hide)
414                 th.addClass('invisible');
415             else
416                 th.removeClass('invisible');
417         }
418     },
420     getColumnPos: function(columnName) {
421         for (var i = 0; i < this.columns.length; ++i)
422             if (this.columns[i].name == columnName)
423                 return i;
424         return -1;
425     },
427     updateColumn: function(columnName) {
428         var pos = this.getColumnPos(columnName);
429         var visible = ((this.columns[pos].visible != '0') && !this.columns[pos].force_hide);
430         var ths = this.hiddenTableHeader.getElements('th');
431         var fths = this.fixedTableHeader.getElements('th');
432         var trs = this.tableBody.getElements('tr');
433         var style = 'width: ' + this.columns[pos].width + 'px;' + this.columns[pos].style;
435         ths[pos].setAttribute('style', style);
436         fths[pos].setAttribute('style', style);
438         if (visible) {
439             ths[pos].removeClass('invisible');
440             fths[pos].removeClass('invisible');
441             for (var i = 0; i < trs.length; ++i)
442                 trs[i].getElements('td')[pos].removeClass('invisible');
443         }
444         else {
445             ths[pos].addClass('invisible');
446             fths[pos].addClass('invisible');
447             for (var j = 0; j < trs.length; ++j)
448                 trs[j].getElements('td')[pos].addClass('invisible');
449         }
450         if (this.columns[pos].onResize !== null) {
451             this.columns[pos].onResize(columnName);
452         }
453     },
455     getSortedColunn: function() {
456         return localStorage.getItem('sorted_column_' + this.dynamicTableDivId);
457     },
459     setSortedColumn: function(column) {
460         if (column != this.sortedColumn) {
461             var oldColumn = this.sortedColumn;
462             this.sortedColumn = column;
463             this.reverseSort = '0';
464             this.setSortedColumnIcon(column, oldColumn, false);
465         }
466         else {
467             // Toggle sort order
468             this.reverseSort = this.reverseSort == '0' ? '1' : '0';
469             this.setSortedColumnIcon(column, null, (this.reverseSort === '1'));
470         }
471         localStorage.setItem('sorted_column_' + this.dynamicTableDivId, column);
472         localStorage.setItem('reverse_sort_' + this.dynamicTableDivId, this.reverseSort);
473         this.updateTable(false);
474     },
476     setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
477         var getCol = function(headerDivId, colName) {
478             var colElem = $$("#" + headerDivId + " .column_" + colName);
479             if (colElem.length == 1)
480                 return colElem[0];
481             return null;
482         };
484         var colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
485         if (colElem !== null) {
486             colElem.addClass('sorted');
487             if (isReverse)
488                 colElem.addClass('reverse');
489             else
490                 colElem.removeClass('reverse');
491         }
492         var oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
493         if (oldColElem !== null) {
494             oldColElem.removeClass('sorted');
495             oldColElem.removeClass('reverse');
496         }
497     },
499     getSelectedRowId: function() {
500         if (this.selectedRows.length > 0)
501             return this.selectedRows[0];
502         return '';
503     },
505     isRowSelected: function(rowId) {
506         return this.selectedRows.contains(rowId);
507     },
509     altRow: function() {
510         if (!MUI.ieLegacySupport)
511             return;
513         var trs = this.tableBody.getElements('tr');
514         trs.each(function(el, i) {
515             if (i % 2) {
516                 el.addClass('alt');
517             }
518             else {
519                 el.removeClass('alt');
520             }
521         }.bind(this));
522     },
524     selectAll: function() {
525         this.deselectAll();
527         var trs = this.tableBody.getElements('tr');
528         for (var i = 0; i < trs.length; ++i) {
529             var tr = trs[i];
530             this.selectedRows.push(tr.rowId);
531             if (!tr.hasClass('selected'))
532                 tr.addClass('selected');
533         }
534     },
536     deselectAll: function() {
537         this.selectedRows.empty();
538     },
540     selectRow: function(rowId) {
541         this.selectedRows.push(rowId);
542         this.setRowClass();
543         this.onSelectedRowChanged();
544     },
546     deselectRow: function(rowId) {
547         this.selectedRows.erase(rowId);
548         this.setRowClass();
549         this.onSelectedRowChanged();
550     },
552     selectRows: function(rowId1, rowId2) {
553         this.deselectAll();
554         if (rowId1 === rowId2) {
555             this.selectRow(rowId1);
556             return;
557         }
559         var select = false;
560         var that = this;
561         this.tableBody.getElements('tr').each(function(tr) {
562             if ((tr.rowId == rowId1) || (tr.rowId == rowId2)) {
563                 select = !select;
564                 that.selectedRows.push(tr.rowId);
565             }
566             else if (select) {
567                 that.selectedRows.push(tr.rowId);
568             }
569         });
570         this.setRowClass();
571         this.onSelectedRowChanged();
572     },
574     reselectRows: function(rowIds) {
575         this.deselectAll();
576         this.selectedRows = rowIds.slice();
577         this.tableBody.getElements('tr').each(function(tr) {
578             if (rowIds.indexOf(tr.rowId) > -1)
579                 tr.addClass('selected');
580         });
581     },
583     setRowClass: function() {
584         var that = this;
585         this.tableBody.getElements('tr').each(function(tr) {
586             if (that.isRowSelected(tr.rowId))
587                 tr.addClass('selected');
588             else
589                 tr.removeClass('selected');
590         });
591     },
593     onSelectedRowChanged: function() {},
595     updateRowData: function(data) {
596         var rowId = data['rowId'];
597         var row;
599         if (!this.rows.has(rowId)) {
600             row = {};
601             this.rows.set(rowId, row);
602             row['full_data'] = {};
603             row['rowId'] = rowId;
604         }
605         else
606             row = this.rows.get(rowId);
608         row['data'] = data;
610         for (var x in data)
611             row['full_data'][x] = data[x];
612     },
614     getFilteredAndSortedRows: function() {
615         var filteredRows = [];
617         var rows = this.rows.getValues();
619         for (var i = 0; i < rows.length; ++i) {
620             filteredRows.push(rows[i]);
621             filteredRows[rows[i].rowId] = rows[i];
622         }
624         filteredRows.sort(function(row1, row2) {
625             var column = this.columns[this.sortedColumn];
626             var res = column.compareRows(row1, row2);
627             if (this.reverseSort == '0')
628                 return res;
629             else
630                 return -res;
631         }.bind(this));
632         return filteredRows;
633     },
635     getTrByRowId: function(rowId) {
636         var trs = this.tableBody.getElements('tr');
637         for (var i = 0; i < trs.length; ++i)
638             if (trs[i].rowId == rowId)
639                 return trs[i];
640         return null;
641     },
643     updateTable: function(fullUpdate) {
644         if (fullUpdate === undefined)
645             fullUpdate = false;
647         var rows = this.getFilteredAndSortedRows();
649         for (var i = 0; i < this.selectedRows.length; ++i)
650             if (!(this.selectedRows[i] in rows)) {
651                 this.selectedRows.splice(i, 1);
652                 --i;
653             }
655         var trs = this.tableBody.getElements('tr');
657         for (var rowPos = 0; rowPos < rows.length; ++rowPos) {
658             var rowId = rows[rowPos]['rowId'];
659             var tr_found = false;
660             for (var j = rowPos; j < trs.length; ++j)
661                 if (trs[j]['rowId'] == rowId) {
662                     tr_found = true;
663                     if (rowPos == j)
664                         break;
665                     trs[j].inject(trs[rowPos], 'before');
666                     var tmpTr = trs[j];
667                     trs.splice(j, 1);
668                     trs.splice(rowPos, 0, tmpTr);
669                     break;
670                 }
671             if (tr_found) // row already exists in the table
672                 this.updateRow(trs[rowPos], fullUpdate);
673             else { // else create a new row in the table
674                 var tr = new Element('tr');
676                 tr['rowId'] = rows[rowPos]['rowId'];
678                 tr._this = this;
679                 tr.addEvent('contextmenu', function(e) {
680                     if (!this._this.isRowSelected(this.rowId)) {
681                         this._this.deselectAll();
682                         this._this.selectRow(this.rowId);
683                     }
684                     return true;
685                 });
686                 tr.addEvent('click', function(e) {
687                     e.stop();
688                     if (e.control || e.meta) {
689                         // CTRL/CMD âŒ˜ key was pressed
690                         if (this._this.isRowSelected(this.rowId))
691                             this._this.deselectRow(this.rowId);
692                         else
693                             this._this.selectRow(this.rowId);
694                     }
695                     else if (e.shift && (this._this.selectedRows.length == 1)) {
696                         // Shift key was pressed
697                         this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
698                     }
699                     else {
700                         // Simple selection
701                         this._this.deselectAll();
702                         this._this.selectRow(this.rowId);
703                     }
704                     return false;
705                 });
707                 this.setupTr(tr);
709                 for (var k = 0; k < this.columns.length; ++k) {
710                     var td = new Element('td');
711                     if ((this.columns[k].visible == '0') || this.columns[k].force_hide)
712                         td.addClass('invisible');
713                     td.injectInside(tr);
714                 }
716                 // Insert
717                 if (rowPos >= trs.length) {
718                     tr.inject(this.tableBody);
719                     trs.push(tr);
720                 }
721                 else {
722                     tr.inject(trs[rowPos], 'before');
723                     trs.splice(rowPos, 0, tr);
724                 }
726                 // Update context menu
727                 if (this.contextMenu)
728                     this.contextMenu.addTarget(tr);
730                 this.updateRow(tr, true);
731             }
732         }
734         rowPos = rows.length;
736         while ((rowPos < trs.length) && (trs.length > 0)) {
737             trs[trs.length - 1].dispose();
738             trs.pop();
739         }
740     },
742     setupTr: function(tr) {},
744     updateRow: function(tr, fullUpdate) {
745         var row = this.rows.get(tr.rowId);
746         var data = row[fullUpdate ? 'full_data' : 'data'];
748         var tds = tr.getElements('td');
749         for (var i = 0; i < this.columns.length; ++i) {
750             if (data.hasOwnProperty(this.columns[i].dataProperties[0]))
751                 this.columns[i].updateTd(tds[i], row);
752         }
753         row['data'] = {};
754     },
756     removeRow: function(rowId) {
757         this.selectedRows.erase(rowId);
758         var tr = this.getTrByRowId(rowId);
759         if (tr !== null) {
760             tr.dispose();
761             this.rows.erase(rowId);
762             return true;
763         }
764         return false;
765     },
767     clear: function() {
768         this.deselectAll();
769         this.rows.empty();
770         var trs = this.tableBody.getElements('tr');
771         while (trs.length > 0) {
772             trs[trs.length - 1].dispose();
773             trs.pop();
774         }
775     },
777     selectedRowsIds: function() {
778         return this.selectedRows.slice();
779     },
781     getRowIds: function() {
782         return this.rows.getKeys();
783     },
786 var TorrentsTable = new Class({
787     Extends: DynamicTable,
789     initColumns: function() {
790         this.newColumn('priority', '', '#', 30, true);
791         this.newColumn('state_icon', 'cursor: default', '', 22, true);
792         this.newColumn('name', '', 'QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]', 200, true);
793         this.newColumn('size', '', 'QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]', 100, true);
794         this.newColumn('total_size', '', 'QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]', 100, false);
795         this.newColumn('progress', '', 'QBT_TR(Done)QBT_TR[CONTEXT=TransferListModel]', 85, true);
796         this.newColumn('status', '', 'QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]', 100, true);
797         this.newColumn('num_seeds', '', 'QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]', 100, true);
798         this.newColumn('num_leechs', '', 'QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]', 100, true);
799         this.newColumn('dlspeed', '', 'QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]', 100, true);
800         this.newColumn('upspeed', '', 'QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]', 100, true);
801         this.newColumn('eta', '', 'QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]', 100, true);
802         this.newColumn('ratio', '', 'QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]', 100, true);
803         this.newColumn('category', '', 'QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]', 100, true);
804         this.newColumn('tags', '', 'QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]', 100, true);
805         this.newColumn('added_on', '', 'QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]', 100, true);
806         this.newColumn('completion_on', '', 'QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]', 100, false);
807         this.newColumn('tracker', '', 'QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]', 100, false);
808         this.newColumn('dl_limit', '', 'QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
809         this.newColumn('up_limit', '', 'QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
810         this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]', 100, false);
811         this.newColumn('uploaded', '', 'QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]', 100, false);
812         this.newColumn('downloaded_session', '', 'QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]', 100, false);
813         this.newColumn('uploaded_session', '', 'QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]', 100, false);
814         this.newColumn('amount_left', '', 'QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]', 100, false);
815         this.newColumn('time_active', '', 'QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]', 100, false);
816         this.newColumn('save_path', '', 'QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]', 100, false);
817         this.newColumn('completed', '', 'QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]', 100, false);
818         this.newColumn('max_ratio', '', 'QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
819         this.newColumn('seen_complete', '', 'QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]', 100, false);
820         this.newColumn('last_activity', '', 'QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]', 100, false);
822         this.columns['state_icon'].onclick = '';
823         this.columns['state_icon'].dataProperties[0] = 'state';
825         this.columns['num_seeds'].dataProperties.push('num_complete');
827         this.columns['num_leechs'].dataProperties.push('num_incomplete');
829         this.initColumnsFunctions();
830     },
832     initColumnsFunctions: function() {
834         // state_icon
835         this.columns['state_icon'].updateTd = function(td, row) {
836             var state = this.getRowValue(row);
837             // normalize states
838             switch (state) {
839                 case "forcedDL":
840                 case "metaDL":
841                     state = "downloading";
842                     break;
843                 case "allocating":
844                     state = "stalledDL";
845                     break;
846                 case "forcedUP":
847                     state = "uploading";
848                     break;
849                 case "pausedDL":
850                     state = "paused";
851                     break;
852                 case "pausedUP":
853                     state = "completed";
854                     break;
855                 case "queuedDL":
856                 case "queuedUP":
857                     state = "queued";
858                     break;
859                 case "checkingDL":
860                 case "checkingUP":
861                 case "queuedForChecking":
862                 case "checkingResumeData":
863                 case "moving":
864                     state = "checking";
865                     break;
866                 case "unknown":
867                 case "missingFiles":
868                     state = "error";
869                     break;
870                 default:
871                     break; // do nothing
872             }
874             var img_path = 'images/skin/' + state + '.svg';
876             if (td.getChildren('img').length) {
877                 var img = td.getChildren('img')[0];
878                 if (img.src.indexOf(img_path) < 0)
879                     img.set('src', img_path);
880             }
881             else
882                 td.adopt(new Element('img', {
883                     'src': img_path,
884                     'class': 'stateIcon'
885                 }));
886         };
888         // status
889         this.columns['status'].updateTd = function(td, row) {
890             var state = this.getRowValue(row);
891             if (!state) return;
893             var status;
894             switch (state) {
895                 case "downloading":
896                     status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
897                     break;
898                 case "stalledDL":
899                     status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
900                     break;
901                 case "metaDL":
902                     status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
903                     break;
904                 case "forcedDL":
905                     status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
906                     break;
907                 case "allocating":
908                     status = "QBT_TR(Allocating)QBT_TR[CONTEXT=TransferListDelegate]";
909                     break;
910                 case "uploading":
911                 case "stalledUP":
912                     status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
913                     break;
914                 case "forcedUP":
915                     status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
916                     break;
917                 case "queuedDL":
918                 case "queuedUP":
919                     status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
920                     break;
921                 case "checkingDL":
922                 case "checkingUP":
923                     status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
924                     break;
925                 case "queuedForChecking":
926                     status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
927                     break;
928                 case "checkingResumeData":
929                     status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
930                     break;
931                 case "pausedDL":
932                     status = "QBT_TR(Paused)QBT_TR[CONTEXT=TransferListDelegate]";
933                     break;
934                 case "pausedUP":
935                     status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
936                     break;
937                 case "moving":
938                     status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
939                     break;
940                 case "missingFiles":
941                     status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
942                     break;
943                 case "error":
944                     status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
945                     break;
946                 default:
947                     status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
948             }
950             td.set('html', status);
951         };
953         // priority
954         this.columns['priority'].updateTd = function(td, row) {
955             var priority = this.getRowValue(row);
956             td.set('html', priority < 1 ? '*' : priority);
957         };
959         this.columns['priority'].compareRows = function(row1, row2) {
960             var row1_val = this.getRowValue(row1);
961             var row2_val = this.getRowValue(row2);
962             if (row1_val < 1)
963                 row1_val = 1000000;
964             if (row2_val < 1)
965                 row2_val = 1000000;
966             if (row1_val < row2_val)
967                 return -1;
968             else if (row1_val > row2_val)
969                 return 1;
970             else return 0;
971         };
973         // name, category
974         this.columns['name'].updateTd = function(td, row) {
975             td.set('html', escapeHtml(this.getRowValue(row)));
976         };
977         this.columns['category'].updateTd = this.columns['name'].updateTd;
979         // size
980         this.columns['size'].updateTd = function(td, row) {
981             var size = this.getRowValue(row);
982             td.set('html', friendlyUnit(size, false));
983         };
985         // progress
986         this.columns['progress'].updateTd = function(td, row) {
987             var progress = this.getRowValue(row);
988             var progressFormated = (progress * 100).round(1);
989             if (progressFormated == 100.0 && progress != 1.0)
990                 progressFormated = 99.9;
992             if (td.getChildren('div').length) {
993                 var div = td.getChildren('div')[0];
994                 if (td.resized) {
995                     td.resized = false;
996                     div.setWidth(ProgressColumnWidth - 5);
997                 }
998                 if (div.getValue() != progressFormated)
999                     div.setValue(progressFormated);
1000             }
1001             else {
1002                 if (ProgressColumnWidth < 0)
1003                     ProgressColumnWidth = td.offsetWidth;
1004                 td.adopt(new ProgressBar(progressFormated.toFloat(), {
1005                     'width': ProgressColumnWidth - 5
1006                 }));
1007                 td.resized = false;
1008             }
1009         };
1011         this.columns['progress'].onResize = function(columnName) {
1012             var pos = this.getColumnPos(columnName);
1013             var trs = this.tableBody.getElements('tr');
1014             ProgressColumnWidth = -1;
1015             for (var i = 0; i < trs.length; ++i) {
1016                 var td = trs[i].getElements('td')[pos];
1017                 if (ProgressColumnWidth < 0)
1018                     ProgressColumnWidth = td.offsetWidth;
1019                 td.resized = true;
1020                 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1021             }
1022         }.bind(this);
1024         // num_seeds
1025         this.columns['num_seeds'].updateTd = function(td, row) {
1026             var num_seeds = this.getRowValue(row, 0);
1027             var num_complete = this.getRowValue(row, 1);
1028             var html = num_seeds;
1029             if (num_complete != -1)
1030                 html += ' (' + num_complete + ')';
1031             td.set('html', html);
1032         };
1033         this.columns['num_seeds'].compareRows = function(row1, row2) {
1034             var num_seeds1 = this.getRowValue(row1, 0);
1035             var num_complete1 = this.getRowValue(row1, 1);
1037             var num_seeds2 = this.getRowValue(row2, 0);
1038             var num_complete2 = this.getRowValue(row2, 1);
1040             if (num_complete1 < num_complete2)
1041                 return -1;
1042             else if (num_complete1 > num_complete2)
1043                 return 1;
1044             else if (num_seeds1 < num_seeds2)
1045                 return -1;
1046             else if (num_seeds1 > num_seeds2)
1047                 return 1;
1048             else return 0;
1049         };
1051         // num_leechs
1052         this.columns['num_leechs'].updateTd = this.columns['num_seeds'].updateTd;
1053         this.columns['num_leechs'].compareRows = this.columns['num_seeds'].compareRows;
1055         // dlspeed
1056         this.columns['dlspeed'].updateTd = function(td, row) {
1057             var speed = this.getRowValue(row);
1058             td.set('html', friendlyUnit(speed, true));
1059         };
1061         // upspeed
1062         this.columns['upspeed'].updateTd = this.columns['dlspeed'].updateTd;
1064         // eta
1065         this.columns['eta'].updateTd = function(td, row) {
1066             var eta = this.getRowValue(row);
1067             td.set('html', friendlyDuration(eta));
1068         };
1070         // ratio
1071         this.columns['ratio'].updateTd = function(td, row) {
1072             var ratio = this.getRowValue(row);
1073             var html = null;
1074             if (ratio == -1)
1075                 html = '∞';
1076             else
1077                 html = (Math.floor(100 * ratio) / 100).toFixed(2); //Don't round up
1078             td.set('html', html);
1079         };
1081         // tags
1082         this.columns['tags'].updateTd = this.columns['name'].updateTd;
1084         // added on
1085         this.columns['added_on'].updateTd = function(td, row) {
1086             var date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1087             td.set('html', date);
1088         };
1090         // completion_on
1091         this.columns['completion_on'].updateTd = function(td, row) {
1092             var val = this.getRowValue(row);
1093             if (val === 0xffffffff || val < 0)
1094                 td.set('html', '');
1095             else {
1096                 var date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1097                 td.set('html', date);
1098             }
1099         };
1101         // seen_complete
1102         this.columns['seen_complete'].updateTd = this.columns['completion_on'].updateTd;
1104         //  dl_limit, up_limit
1105         this.columns['dl_limit'].updateTd = function(td, row) {
1106             var speed = this.getRowValue(row);
1107             if (speed === 0)
1108                 td.set('html', '∞');
1109             else
1110                 td.set('html', friendlyUnit(speed, true));
1111         };
1113         this.columns['up_limit'].updateTd = this.columns['dl_limit'].updateTd;
1115         // downloaded, uploaded, downloaded_session, uploaded_session, amount_left, completed, total_size
1116         this.columns['downloaded'].updateTd = this.columns['size'].updateTd;
1117         this.columns['uploaded'].updateTd = this.columns['size'].updateTd;
1118         this.columns['downloaded_session'].updateTd = this.columns['size'].updateTd;
1119         this.columns['uploaded_session'].updateTd = this.columns['size'].updateTd;
1120         this.columns['amount_left'].updateTd = this.columns['size'].updateTd;
1121         this.columns['amount_left'].updateTd = this.columns['size'].updateTd;
1122         this.columns['completed'].updateTd = this.columns['size'].updateTd;
1123         this.columns['total_size'].updateTd = this.columns['size'].updateTd;
1125         // save_path, tracker
1126         this.columns['save_path'].updateTd = this.columns['name'].updateTd;
1127         this.columns['tracker'].updateTd = this.columns['name'].updateTd;
1129         // max_ratio
1130         this.columns['max_ratio'].updateTd = this.columns['ratio'].updateTd;
1132         // last_activity
1133         this.columns['last_activity'].updateTd = function(td, row) {
1134             var val = this.getRowValue(row);
1135             if (val < 1)
1136                 td.set('html', '∞');
1137             else
1138                 td.set('html', 'QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]'.replace('%1', friendlyDuration((new Date()) / 1000 - val)));
1139         };
1141         // time active
1142         this.columns['time_active'].updateTd = function(td, row) {
1143             var time = this.getRowValue(row);
1144             td.set('html', friendlyDuration(time));
1145         };
1146     },
1148     applyFilter: function(row, filterName, categoryHash, filterTerms) {
1149         var state = row['full_data'].state;
1150         var name = row['full_data'].name.toLowerCase();
1151         var inactive = false;
1152         var r;
1154         switch (filterName) {
1155             case 'downloading':
1156                 if (state != 'downloading' && !~state.indexOf('DL'))
1157                     return false;
1158                 break;
1159             case 'seeding':
1160                 if (state != 'uploading' && state != 'forcedUP' && state != 'stalledUP' && state != 'queuedUP' && state != 'checkingUP')
1161                     return false;
1162                 break;
1163             case 'completed':
1164                 if (state != 'uploading' && !~state.indexOf('UP'))
1165                     return false;
1166                 break;
1167             case 'paused':
1168                 if (!~state.indexOf('paused'))
1169                     return false;
1170                 break;
1171             case 'resumed':
1172                 if (~state.indexOf('paused'))
1173                     return false;
1174                 break;
1175             case 'inactive':
1176                 inactive = true;
1177                 // fallthrough
1178             case 'active':
1179                 if (state == 'stalledDL')
1180                     r = (row['full_data'].upspeed > 0);
1181                 else
1182                     r = state == 'metaDL' || state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP';
1183                 if (r == inactive)
1184                     return false;
1185                 break;
1186             case 'errored':
1187                 if (state != 'error' && state != "unknown" && state != "missingFiles")
1188                     return false;
1189                 break;
1190         }
1192         var categoryHashInt = parseInt(categoryHash);
1193         if (!isNaN(categoryHashInt)) {
1194             switch (categoryHashInt) {
1195                 case CATEGORIES_ALL:
1196                     break;  // do nothing
1197                 case CATEGORIES_UNCATEGORIZED:
1198                     if (row['full_data'].category.length !== 0)
1199                         return false
1200                     break;  // do nothing
1201                 default:
1202                     if (categoryHashInt !== genHash(row['full_data'].category))
1203                         return false;
1204             }
1205         }
1207         if (filterTerms) {
1208             for (var i = 0; i < filterTerms.length; ++i) {
1209                 if (name.indexOf(filterTerms[i]) === -1)
1210                     return false;
1211             }
1212         }
1214         return true;
1215     },
1217     getFilteredTorrentsNumber: function(filterName, categoryHash) {
1218         var cnt = 0;
1219         var rows = this.rows.getValues();
1221         for (var i = 0; i < rows.length; ++i)
1222             if (this.applyFilter(rows[i], filterName, categoryHash, null)) ++cnt;
1223         return cnt;
1224     },
1226     getFilteredTorrentsHashes: function(filterName, categoryHash) {
1227         var rowsHashes = [];
1228         var rows = this.rows.getValues();
1230         for (var i = 0; i < rows.length; ++i)
1231             if (this.applyFilter(rows[i], filterName, categoryHash, null))
1232                 rowsHashes.push(rows[i]['rowId']);
1234         return rowsHashes;
1235     },
1237     getFilteredAndSortedRows: function() {
1238         var filteredRows = [];
1240         var rows = this.rows.getValues();
1241         var filterText = $('torrentsFilterInput').value.trim().toLowerCase();
1242         var filterTerms = (filterText.length > 0) ? filterText.split(" ") : null;
1244         for (var i = 0; i < rows.length; ++i) {
1245             if (this.applyFilter(rows[i], selected_filter, selected_category, filterTerms)) {
1246                 filteredRows.push(rows[i]);
1247                 filteredRows[rows[i].rowId] = rows[i];
1248             }
1249         }
1251         filteredRows.sort(function(row1, row2) {
1252             var column = this.columns[this.sortedColumn];
1253             var res = column.compareRows(row1, row2);
1254             if (this.reverseSort == '0')
1255                 return res;
1256             else
1257                 return -res;
1258         }.bind(this));
1259         return filteredRows;
1260     },
1262     setupTr: function(tr) {
1263         tr.addEvent('dblclick', function(e) {
1264             e.stop();
1265             this._this.deselectAll();
1266             this._this.selectRow(this.rowId);
1267             var row = this._this.rows.get(this.rowId);
1268             var state = row['full_data'].state;
1269             if (~state.indexOf('paused'))
1270                 startFN();
1271             else
1272                 pauseFN();
1273             return true;
1274         });
1275         tr.addClass("torrentsTableContextMenuTarget");
1276     },
1278     getCurrentTorrentHash: function() {
1279         return this.getSelectedRowId();
1280     },
1282     onSelectedRowChanged: function() {
1283         updatePropertiesPanel();
1284     }
1287 var TorrentPeersTable = new Class({
1288     Extends: DynamicTable,
1290     initColumns: function() {
1291         this.newColumn('country', '', 'QBT_TR(Country)QBT_TR[CONTEXT=PeerListWidget]', 22, true);
1292         this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]', 80, true);
1293         this.newColumn('port', '', 'QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]', 35, true);
1294         this.newColumn('connection', '', 'QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1295         this.newColumn('flags', '', 'QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1296         this.newColumn('client', '', 'QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]', 140, true);
1297         this.newColumn('progress', '', 'QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1298         this.newColumn('dl_speed', '', 'QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1299         this.newColumn('up_speed', '', 'QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1300         this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1301         this.newColumn('uploaded', '', 'QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1302         this.newColumn('relevance', '', 'QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]', 30, true);
1303         this.newColumn('files', '', 'QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]', 100, true);
1305         this.columns['country'].dataProperties.push('country_code');
1306         this.columns['flags'].dataProperties.push('flags_desc');
1307         this.initColumnsFunctions();
1308     },
1310     initColumnsFunctions: function() {
1312         // country
1314         this.columns['country'].updateTd = function(td, row) {
1315             var country = this.getRowValue(row, 0);
1316             var country_code = this.getRowValue(row, 1);
1318             if (!country_code) {
1319                 if (td.getChildren('img').length)
1320                     td.getChildren('img')[0].dispose();
1321                 return;
1322             }
1324             var img_path = 'images/flags/' + country_code + '.svg';
1326             if (td.getChildren('img').length) {
1327                 var img = td.getChildren('img')[0];
1328                 img.set('src', img_path);
1329                 img.set('class', 'flags');
1330                 img.set('alt', country);
1331                 img.set('title', country);
1332             }
1333             else
1334                 td.adopt(new Element('img', {
1335                     'src': img_path,
1336                     'class': 'flags',
1337                     'alt': country,
1338                     'title': country
1339                 }));
1340         };
1342         // ip
1344         this.columns['ip'].compareRows = function(row1, row2) {
1345             var ip1 = this.getRowValue(row1);
1346             var ip2 = this.getRowValue(row2);
1348             var a = ip1.split(".");
1349             var b = ip2.split(".");
1351             for (var i = 0; i < 4; ++i) {
1352                 if (a[i] != b[i])
1353                     return a[i] - b[i];
1354             }
1356             return 0;
1357         };
1359         // progress, relevance
1361         this.columns['progress'].updateTd = function(td, row) {
1362             var progress = this.getRowValue(row);
1363             var progressFormated = (progress * 100).round(1);
1364             if (progressFormated == 100.0 && progress != 1.0)
1365                 progressFormated = 99.9;
1366             progressFormated += "%";
1367             td.set('html', progressFormated);
1368         };
1370         this.columns['relevance'].updateTd = this.columns['progress'].updateTd;
1372         // dl_speed, up_speed
1374         this.columns['dl_speed'].updateTd = function(td, row) {
1375             var speed = this.getRowValue(row);
1376             if (speed === 0)
1377                 td.set('html', '');
1378             else
1379                 td.set('html', friendlyUnit(speed, true));
1380         };
1382         this.columns['up_speed'].updateTd = this.columns['dl_speed'].updateTd;
1384         // downloaded, uploaded
1386         this.columns['downloaded'].updateTd = function(td, row) {
1387             var downloaded = this.getRowValue(row);
1388             td.set('html', friendlyUnit(downloaded, false));
1389         };
1391         this.columns['uploaded'].updateTd = this.columns['downloaded'].updateTd;
1393         // flags
1395         this.columns['flags'].updateTd = function(td, row) {
1396             td.innerHTML = this.getRowValue(row, 0);
1397             td.title = this.getRowValue(row, 1);
1398         };
1400         // files
1402         this.columns['files'].updateTd = function(td, row) {
1403             td.innerHTML = escapeHtml(this.getRowValue(row, 0).replace('\n', ';'));
1404             td.title = escapeHtml(this.getRowValue(row, 0));
1405         };
1407     }
1410 var SearchResultsTable = new Class({
1411     Extends: DynamicTable,
1413     initColumns: function() {
1414         this.newColumn('fileName', '', 'QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]', 500, true);
1415         this.newColumn('fileSize', '', 'QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1416         this.newColumn('nbSeeders', '', 'QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1417         this.newColumn('nbLeechers', '', 'QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1418         this.newColumn('siteUrl', '', 'QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]', 250, true);
1420         this.initColumnsFunctions();
1421     },
1423     initColumnsFunctions: function() {
1424         var displayText = function(td, row) {
1425             var value = this.getRowValue(row);
1426             td.set('html', escapeHtml(value));
1427         }
1428         var displaySize = function(td, row) {
1429             var size = this.getRowValue(row);
1430             td.set('html', friendlyUnit(size, false));
1431         }
1432         var displayNum = function(td, row) {
1433             var value = escapeHtml(this.getRowValue(row));
1434             td.set('html', (value === "-1") ? "Unknown" : value);
1435         }
1437         this.columns['fileName'].updateTd = displayText;
1438         this.columns['fileSize'].updateTd = displaySize;
1439         this.columns['nbSeeders'].updateTd = displayNum;
1440         this.columns['nbLeechers'].updateTd = displayNum;
1441         this.columns['siteUrl'].updateTd = displayText;
1442     },
1444     getFilteredAndSortedRows: function() {
1445         var containsAll = function(text, searchTerms) {
1446             text = text.toLowerCase();
1447             for (var i = 0; i < searchTerms.length; ++i) {
1448                 if (text.indexOf(searchTerms[i].toLowerCase()) === -1)
1449                     return false;
1450             }
1452             return true;
1453         };
1455         var getSizeFilters = function() {
1456             var minSize = (searchSizeFilter.min > 0.00) ? (searchSizeFilter.min * Math.pow(1024, searchSizeFilter.minUnit)) : 0.00;
1457             var maxSize = (searchSizeFilter.max > 0.00) ? (searchSizeFilter.max * Math.pow(1024, searchSizeFilter.maxUnit)) : 0.00;
1459             if ((minSize > maxSize) && (maxSize > 0.00)) {
1460                 var tmp = minSize;
1461                 minSize = maxSize;
1462                 maxSize = tmp;
1463             }
1465             return {
1466                 min: minSize,
1467                 max: maxSize
1468             }
1469         };
1471         var getSeedsFilters = function() {
1472             var minSeeds = (searchSeedsFilter.min > 0) ? searchSeedsFilter.min : 0;
1473             var maxSeeds = (searchSeedsFilter.max > 0) ? searchSeedsFilter.max : 0;
1475             if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1476                 var tmp = minSeeds;
1477                 minSeeds = maxSeeds;
1478                 maxSeeds = tmp;
1479             }
1481             return {
1482                 min: minSeeds,
1483                 max: maxSeeds
1484             }
1485         }
1487         var filteredRows = [];
1488         var rows = this.rows.getValues();
1489         var searchTerms = searchPattern.toLowerCase().split(" ");
1490         var filterTerms = searchFilterPattern.toLowerCase().split(" ");
1491         var sizeFilters = getSizeFilters();
1492         var seedsFilters = getSeedsFilters();
1493         var searchInTorrentName = $('searchInTorrentName').get('value') === "names";
1495         if (searchInTorrentName || filterTerms.length || (searchSizeFilter.min > 0.00) || (searchSizeFilter.max > 0.00)) {
1496             for (var i = 0; i < rows.length; ++i) {
1497                 var row = rows[i];
1499                 if (searchInTorrentName && !containsAll(row.full_data.fileName, searchTerms)) continue;
1500                 if (filterTerms.length && !containsAll(row.full_data.fileName, filterTerms)) continue;
1501                 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min)) continue;
1502                 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max)) continue;
1503                 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min)) continue;
1504                 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max)) continue;
1506                 filteredRows.push(row);
1507             }
1508         }
1509         else {
1510             filteredRows = rows;
1511         }
1513         filteredRows.sort(function(row1, row2) {
1514             var column = this.columns[this.sortedColumn];
1515             var res = column.compareRows(row1, row2);
1516             if (this.reverseSort == '0')
1517                 return res;
1518             else
1519                 return -res;
1520         }.bind(this));
1522         return filteredRows;
1523     },
1525     setupTr: function(tr) {
1526         tr.addClass("searchTableRow");
1527     }
1530 var SearchPluginsTable = new Class({
1531     Extends: DynamicTable,
1533     initColumns: function() {
1534         this.newColumn('fullName', '', 'QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]', 175, true);
1535         this.newColumn('version', '', 'QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]', 100, true);
1536         this.newColumn('url', '', 'QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]', 175, true);
1537         this.newColumn('enabled', '', 'QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]', 100, true);
1539         this.initColumnsFunctions();
1540     },
1542     initColumnsFunctions: function() {
1543         var displayText = function(td, row) {
1544             var value = this.getRowValue(row);
1545             td.set('html', escapeHtml(value));
1546         }
1548         this.columns['fullName'].updateTd = displayText;
1549         this.columns['version'].updateTd = displayText;
1550         this.columns['url'].updateTd = displayText;
1551         this.columns['enabled'].updateTd = function(td, row) {
1552             var value = this.getRowValue(row);
1553             if (value) {
1554                 td.set('html', "Yes");
1555                 td.getParent("tr").addClass("green");
1556                 td.getParent("tr").removeClass("red");
1557             }
1558             else {
1559                 td.set('html', "No");
1560                 td.getParent("tr").addClass("red");
1561                 td.getParent("tr").removeClass("green");
1562             }
1563         };
1564     },
1566     setupTr: function(tr) {
1567         tr.addClass("searchPluginsTableRow");
1568     }
1571 var TorrentTrackersTable = new Class({
1572     Extends: DynamicTable,
1574     initColumns: function() {
1575         this.newColumn('tier', '', 'QBT_TR(#)QBT_TR[CONTEXT=TrackerListWidget]', 35, true);
1576         this.newColumn('url', '', 'QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]', 250, true);
1577         this.newColumn('status', '', 'QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]', 125, true);
1578         this.newColumn('peers', '', 'QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1579         this.newColumn('seeds', '', 'QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1580         this.newColumn('leeches', '', 'QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1581         this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=TrackerListWidget]', 100, true);
1582         this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]', 250, true);
1583     },
1586 var TorrentFilesTable = new Class({
1587     Extends: DynamicTable,
1589     initColumns: function() {
1590         this.newColumn('checked', '', '', 50, true);
1591         this.newColumn('name', '', 'QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]', 300, true);
1592         this.newColumn('size', '', 'QBT_TR(Size)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1593         this.newColumn('progress', '', 'QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]', 100, true);
1594         this.newColumn('priority', '', 'QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]', 150, true);
1595         this.newColumn('remaining', '', 'QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1596         this.newColumn('availability', '', 'QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1598         this.initColumnsFunctions();
1599     },
1601     initColumnsFunctions: function() {
1602         var displaySize = function(td, row) {
1603             var size = this.getRowValue(row);
1604             td.set('html', friendlyUnit(size, false));
1605         }
1606         var displayPercentage = function(td, row) {
1607             var value = this.getRowValue(row);
1608             td.set('html', friendlyPercentage(value));
1609         };
1611         this.columns['checked'].updateTd = function(td, row) {
1612             var id = row.rowId;
1613             var value = this.getRowValue(row);
1615             if (isDownloadCheckboxExists(id)) {
1616                 updateDownloadCheckbox(id, value);
1617             }
1618             else {
1619                 var treeImg = new Element('img', {
1620                     src: 'images/L.gif',
1621                     style: 'margin-bottom: -2px'
1622                 });
1623                 td.adopt(treeImg, createDownloadCheckbox(row.rowId, value));
1624             }
1625         };
1627         this.columns['size'].updateTd = displaySize;
1629         this.columns['progress'].updateTd = function(td, row) {
1630             var id = row.rowId;
1631             var value = this.getRowValue(row);
1633             var progressBar = $('pbf_' + id);
1634             if (progressBar === null) {
1635                 td.adopt(new ProgressBar(value.toFloat(), {
1636                     'id': 'pbf_' + id,
1637                     'width': 80
1638                 }));
1639             }
1640             else {
1641                 progressBar.setValue(value.toFloat());
1642             }
1643         };
1645         this.columns['priority'].updateTd = function(td, row) {
1646             var id = row.rowId;
1647             var value = this.getRowValue(row);
1649             if (isPriorityComboExists(id))
1650                 updatePriorityCombo(id, value);
1651             else
1652                 td.adopt(createPriorityCombo(id, value));
1653         };
1655         this.columns['remaining'].updateTd = displaySize;
1656         this.columns['availability'].updateTd = displayPercentage;
1657     }
1660 /*************************************************************/