3 * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>,
4 * Christophe Dumez <chris@qbittorrent.org>
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 this.torrentsTable
= new TorrentsTable();
28 const torrentTrackersTable
= new TorrentTrackersTable();
29 const torrentPeersTable
= new TorrentPeersTable();
30 const torrentFilesTable
= new TorrentFilesTable();
31 const searchResultsTable
= new SearchResultsTable();
32 const searchPluginsTable
= new SearchPluginsTable();
34 let updatePropertiesPanel = function() {};
36 let updateTorrentData = function() {};
37 let updateTrackersData = function() {};
38 let updateTorrentPeersData = function() {};
39 let updateWebSeedsData = function() {};
40 let updateTorrentFilesData = function() {};
42 this.updateMainData = function() {};
43 let alternativeSpeedLimits
= false;
44 let queueing_enabled
= true;
45 let serverSyncMainDataInterval
= 1500;
46 let customSyncMainDataInterval
= null;
47 let searchTabInitialized
= false;
51 const CATEGORIES_ALL
= 1;
52 const CATEGORIES_UNCATEGORIZED
= 2;
54 let category_list
= {};
56 let selected_category
= CATEGORIES_ALL
;
57 let setCategoryFilter = function() {};
60 const TAGS_UNTAGGED
= 2;
64 let selectedTag
= TAGS_ALL
;
65 let setTagFilter = function() {};
67 let selected_filter
= getLocalStorageItem('selected_filter', 'all');
68 let setFilter = function() {};
69 let toggleFilterDisplay = function() {};
71 const loadSelectedCategory = function() {
72 selected_category
= getLocalStorageItem('selected_category', CATEGORIES_ALL
);
74 loadSelectedCategory();
76 const loadSelectedTag = function() {
77 selectedTag
= getLocalStorageItem('selected_tag', TAGS_ALL
);
81 function genHash(string
) {
83 for (let i
= 0; i
< string
.length
; ++i
) {
84 const c
= string
.charCodeAt(i
);
85 hash
= (c
+ hash
* 31) | 0;
90 function getSyncMainDataInterval() {
91 return customSyncMainDataInterval
? customSyncMainDataInterval
: serverSyncMainDataInterval
;
94 const fetchQbtVersion = function() {
96 url
: 'api/v2/app/version',
98 onSuccess: function(info
) {
100 sessionStorage
.setItem('qbtVersion', info
);
106 const qbtVersion = function() {
107 const version
= sessionStorage
.getItem('qbtVersion');
113 window
.addEvent('load', function() {
115 const saveColumnSizes = function() {
116 const filters_width
= $('Filters').getSize().x
;
117 const properties_height_rel
= $('propertiesPanel').getSize().y
/ Window
.getSize().y
;
118 localStorage
.setItem('filters_width', filters_width
);
119 localStorage
.setItem('properties_height_rel', properties_height_rel
);
122 window
.addEvent('resize', function() {
123 // only save sizes if the columns are visible
124 if (!$("mainColumn").hasClass("invisible"))
125 saveColumnSizes
.delay(200); // Resizing might takes some time.
128 /*MochaUI.Desktop = new MochaUI.Desktop();
129 MochaUI.Desktop.desktop.setStyles({
130 'background': '#fff',
131 'visibility': 'visible'
133 MochaUI
.Desktop
.initialize();
135 const buildTransfersTab = function() {
136 let filt_w
= localStorage
.getItem('filters_width');
137 if ($defined(filt_w
))
138 filt_w
= filt_w
.toInt();
144 onResize
: saveColumnSizes
,
146 resizeLimit
: [1, 300]
155 const buildSearchTab = function() {
157 id
: 'searchTabColumn',
163 $("searchTabColumn").addClass("invisible");
168 MochaUI
.initializeTabs('mainWindowTabsList');
170 setCategoryFilter = function(hash
) {
171 selected_category
= hash
;
172 localStorage
.setItem('selected_category', selected_category
);
173 highlightSelectedCategory();
174 if (typeof torrentsTable
.tableBody
!= 'undefined')
178 setTagFilter = function(hash
) {
179 selectedTag
= hash
.toString();
180 localStorage
.setItem('selected_tag', selectedTag
);
181 highlightSelectedTag();
182 if (torrentsTable
.tableBody
!== undefined)
186 setFilter = function(f
) {
187 // Visually Select the right filter
188 $("all_filter").removeClass("selectedFilter");
189 $("downloading_filter").removeClass("selectedFilter");
190 $("seeding_filter").removeClass("selectedFilter");
191 $("completed_filter").removeClass("selectedFilter");
192 $("paused_filter").removeClass("selectedFilter");
193 $("resumed_filter").removeClass("selectedFilter");
194 $("active_filter").removeClass("selectedFilter");
195 $("inactive_filter").removeClass("selectedFilter");
196 $("errored_filter").removeClass("selectedFilter");
197 $(f
+ "_filter").addClass("selectedFilter");
199 localStorage
.setItem('selected_filter', f
);
201 if (typeof torrentsTable
.tableBody
!= 'undefined')
205 toggleFilterDisplay = function(filter
) {
206 const element
= filter
+ "FilterList";
207 localStorage
.setItem('filter_' + filter
+ "_collapsed", !$(element
).hasClass("invisible"));
208 $(element
).toggleClass("invisible")
209 const parent
= $(element
).getParent(".filterWrapper");
210 const toggleIcon
= $(parent
).getChildren(".filterTitle img");
212 toggleIcon
[0].toggleClass("rotate");
226 contentURL
: 'views/filters.html',
227 onContentLoaded: function() {
228 setFilter(selected_filter
);
230 column
: 'filtersColumn',
235 // Show Top Toolbar is enabled by default
236 let showTopToolbar
= true;
237 if (localStorage
.getItem('show_top_toolbar') !== null)
238 showTopToolbar
= localStorage
.getItem('show_top_toolbar') == "true";
239 if (!showTopToolbar
) {
240 $('showTopToolbarLink').firstChild
.style
.opacity
= '0';
241 $('mochaToolbar').addClass('invisible');
244 // Show Status Bar is enabled by default
245 let showStatusBar
= true;
246 if (localStorage
.getItem('show_status_bar') !== null)
247 showStatusBar
= localStorage
.getItem('show_status_bar') === "true";
248 if (!showStatusBar
) {
249 $('showStatusBarLink').firstChild
.style
.opacity
= '0';
250 $('desktopFooterWrapper').addClass('invisible');
253 let speedInTitle
= localStorage
.getItem('speed_in_browser_title_bar') == "true";
255 $('speedInBrowserTitleBarLink').firstChild
.style
.opacity
= '0';
257 // After showing/hiding the toolbar + status bar
258 let showSearchEngine
= localStorage
.getItem('show_search_engine') !== "false";
259 if (!showSearchEngine
) {
260 // uncheck menu option
261 $('showSearchEngineLink').firstChild
.style
.opacity
= '0';
263 $('mainWindowTabs').addClass('invisible');
266 // After Show Top Toolbar
267 MochaUI
.Desktop
.setDesktopSize();
269 let syncMainDataLastResponseId
= 0;
270 const serverState
= {};
272 const removeTorrentFromCategoryList = function(hash
) {
273 if (hash
=== null || hash
=== "")
276 Object
.each(category_list
, function(category
) {
277 if (Object
.contains(category
.torrents
, hash
)) {
279 category
.torrents
.splice(category
.torrents
.indexOf(hash
), 1);
285 const addTorrentToCategoryList = function(torrent
) {
286 const category
= torrent
['category'];
287 if (typeof category
=== 'undefined')
289 if (category
.length
=== 0) { // Empty category
290 removeTorrentFromCategoryList(torrent
['hash']);
293 const categoryHash
= genHash(category
);
294 if (category_list
[categoryHash
] === null) // This should not happen
295 category_list
[categoryHash
] = {
299 if (!Object
.contains(category_list
[categoryHash
].torrents
, torrent
['hash'])) {
300 removeTorrentFromCategoryList(torrent
['hash']);
301 category_list
[categoryHash
].torrents
= category_list
[categoryHash
].torrents
.combine([torrent
['hash']]);
307 const removeTorrentFromTagList = function(hash
) {
308 if ((hash
=== null) || (hash
=== ""))
312 for (const key
in tagList
) {
313 const tag
= tagList
[key
];
314 if (Object
.contains(tag
.torrents
, hash
)) {
316 tag
.torrents
.splice(tag
.torrents
.indexOf(hash
), 1);
322 const addTorrentToTagList = function(torrent
) {
323 if (torrent
['tags'] === undefined) // Tags haven't changed
326 removeTorrentFromTagList(torrent
['hash']);
328 if (torrent
['tags'].length
=== 0) // No tags
331 const tags
= torrent
['tags'].split(',');
333 for (let i
= 0; i
< tags
.length
; ++i
) {
334 const tagHash
= genHash(tags
[i
].trim());
335 if (!Object
.contains(tagList
[tagHash
].torrents
, torrent
['hash'])) {
337 tagList
[tagHash
].torrents
.push(torrent
['hash']);
343 const updateFilter = function(filter
, filterTitle
) {
344 $(filter
+ '_filter').firstChild
.childNodes
[1].nodeValue
= filterTitle
.replace('%1', torrentsTable
.getFilteredTorrentsNumber(filter
, CATEGORIES_ALL
, TAGS_ALL
));
347 const updateFiltersList = function() {
348 updateFilter('all', 'QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
349 updateFilter('downloading', 'QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
350 updateFilter('seeding', 'QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
351 updateFilter('completed', 'QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
352 updateFilter('resumed', 'QBT_TR(Resumed (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
353 updateFilter('paused', 'QBT_TR(Paused (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
354 updateFilter('active', 'QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
355 updateFilter('inactive', 'QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
356 updateFilter('errored', 'QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
359 const updateCategoryList = function() {
360 const categoryList
= $('categoryFilterList');
363 categoryList
.empty();
365 const create_link = function(hash
, text
, count
) {
366 const html
= '<a href="#" onclick="setCategoryFilter(' + hash
+ ');return false;">'
367 + '<img src="images/qbt-theme/inode-directory.svg"/>'
368 + escapeHtml(text
) + ' (' + count
+ ')' + '</a>';
369 const el
= new Element('li', {
373 categoriesFilterContextMenu
.addTarget(el
);
377 const all
= torrentsTable
.getRowIds().length
;
378 let uncategorized
= 0;
379 Object
.each(torrentsTable
.rows
, function(row
) {
380 if (row
['full_data'].category
.length
=== 0)
383 categoryList
.appendChild(create_link(CATEGORIES_ALL
, 'QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]', all
));
384 categoryList
.appendChild(create_link(CATEGORIES_UNCATEGORIZED
, 'QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]', uncategorized
));
386 const sortedCategories
= [];
387 Object
.each(category_list
, function(category
) {
388 sortedCategories
.push(category
.name
);
390 sortedCategories
.sort();
392 Object
.each(sortedCategories
, function(categoryName
) {
393 const categoryHash
= genHash(categoryName
);
394 const categoryCount
= category_list
[categoryHash
].torrents
.length
;
395 categoryList
.appendChild(create_link(categoryHash
, categoryName
, categoryCount
));
398 highlightSelectedCategory();
401 const highlightSelectedCategory = function() {
402 const categoryList
= $('categoryFilterList');
405 const children
= categoryList
.childNodes
;
406 for (let i
= 0; i
< children
.length
; ++i
) {
407 if (children
[i
].id
== selected_category
)
408 children
[i
].className
= "selectedFilter";
410 children
[i
].className
= "";
414 const updateTagList = function() {
415 const tagFilterList
= $('tagFilterList');
416 if (tagFilterList
=== null)
419 while (tagFilterList
.firstChild
!== null)
420 tagFilterList
.removeChild(tagFilterList
.firstChild
);
422 const createLink = function(hash
, text
, count
) {
423 const html
= '<a href="#" onclick="setTagFilter(' + hash
+ ');return false;">'
424 + '<img src="images/qbt-theme/inode-directory.svg"/>'
425 + escapeHtml(text
) + ' (' + count
+ ')' + '</a>';
426 const el
= new Element('li', {
430 tagsFilterContextMenu
.addTarget(el
);
434 const torrentsCount
= torrentsTable
.getRowIds().length
;
436 for (const key
in torrentsTable
.rows
) {
437 if (torrentsTable
.rows
.hasOwnProperty(key
) && torrentsTable
.rows
[key
]['full_data'].tags
.length
=== 0)
440 tagFilterList
.appendChild(createLink(TAGS_ALL
, 'QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]', torrentsCount
));
441 tagFilterList
.appendChild(createLink(TAGS_UNTAGGED
, 'QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]', untagged
));
443 const sortedTags
= [];
444 for (const key
in tagList
)
445 sortedTags
.push(tagList
[key
].name
);
448 for (let i
= 0; i
< sortedTags
.length
; ++i
) {
449 const tagName
= sortedTags
[i
];
450 const tagHash
= genHash(tagName
);
451 const tagCount
= tagList
[tagHash
].torrents
.length
;
452 tagFilterList
.appendChild(createLink(tagHash
, tagName
, tagCount
));
455 highlightSelectedTag();
458 const highlightSelectedTag = function() {
459 const tagFilterList
= $('tagFilterList');
463 const children
= tagFilterList
.childNodes
;
464 for (let i
= 0; i
< children
.length
; ++i
)
465 children
[i
].className
= (children
[i
].id
=== selectedTag
) ? "selectedFilter" : "";
468 let syncMainDataTimer
;
469 const syncMainData = function() {
470 const url
= new URI('api/v2/sync/maindata');
471 url
.setData('rid', syncMainDataLastResponseId
);
476 onFailure: function() {
477 const errorDiv
= $('error_div');
479 errorDiv
.set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]');
480 clearTimeout(syncMainDataTimer
);
481 syncMainDataTimer
= syncMainData
.delay(2000);
483 onSuccess: function(response
) {
484 $('error_div').set('html', '');
486 clearTimeout(torrentsFilterInputTimer
);
487 let torrentsTableSelectedRows
;
488 let update_categories
= false;
489 let updateTags
= false;
490 const full_update
= (response
['full_update'] === true);
492 torrentsTableSelectedRows
= torrentsTable
.selectedRowsIds();
493 torrentsTable
.clear();
497 if (response
['rid']) {
498 syncMainDataLastResponseId
= response
['rid'];
500 if (response
['categories']) {
501 for (const key
in response
['categories']) {
502 const category
= response
['categories'][key
];
503 const categoryHash
= genHash(key
);
504 if (category_list
[categoryHash
] !== undefined) {
505 // only the save path can change for existing categories
506 category_list
[categoryHash
].savePath
= category
.savePath
;
509 category_list
[categoryHash
] = {
511 savePath
: category
.savePath
,
516 update_categories
= true;
518 if (response
['categories_removed']) {
519 response
['categories_removed'].each(function(category
) {
520 const categoryHash
= genHash(category
);
521 delete category_list
[categoryHash
];
523 update_categories
= true;
525 if (response
['tags']) {
526 for (const tag
of response
['tags']) {
527 const tagHash
= genHash(tag
);
528 if (!tagList
[tagHash
]) {
537 if (response
['tags_removed']) {
538 for (let i
= 0; i
< response
['tags_removed'].length
; ++i
) {
539 const tagHash
= genHash(response
['tags_removed'][i
]);
540 delete tagList
[tagHash
];
544 if (response
['torrents']) {
545 let updateTorrentList
= false;
546 for (const key
in response
['torrents']) {
547 response
['torrents'][key
]['hash'] = key
;
548 response
['torrents'][key
]['rowId'] = key
;
549 if (response
['torrents'][key
]['state'])
550 response
['torrents'][key
]['status'] = response
['torrents'][key
]['state'];
551 torrentsTable
.updateRowData(response
['torrents'][key
]);
552 if (addTorrentToCategoryList(response
['torrents'][key
]))
553 update_categories
= true;
554 if (addTorrentToTagList(response
['torrents'][key
]))
556 if (response
['torrents'][key
]['name'])
557 updateTorrentList
= true;
560 if (updateTorrentList
)
561 setupCopyEventHandler();
563 if (response
['torrents_removed'])
564 response
['torrents_removed'].each(function(hash
) {
565 torrentsTable
.removeRow(hash
);
566 removeTorrentFromCategoryList(hash
);
567 update_categories
= true; // Always to update All category
568 removeTorrentFromTagList(hash
);
569 updateTags
= true; // Always to update All tag
571 torrentsTable
.updateTable(full_update
);
572 torrentsTable
.altRow();
573 if (response
['server_state']) {
574 const tmp
= response
['server_state'];
576 serverState
[k
] = tmp
[k
];
577 processServerState();
580 if (update_categories
) {
581 updateCategoryList();
582 torrentsTableContextMenu
.updateCategoriesSubMenu(category_list
);
586 torrentsTableContextMenu
.updateTagsSubMenu(tagList
);
590 // re-select previously selected rows
591 torrentsTable
.reselectRows(torrentsTableSelectedRows
);
593 clearTimeout(syncMainDataTimer
);
594 syncMainDataTimer
= syncMainData
.delay(getSyncMainDataInterval());
599 updateMainData = function() {
600 torrentsTable
.updateTable();
601 clearTimeout(syncMainDataTimer
);
602 syncMainDataTimer
= syncMainData
.delay(100);
605 const processServerState = function() {
606 let transfer_info
= friendlyUnit(serverState
.dl_info_speed
, true);
607 if (serverState
.dl_rate_limit
> 0)
608 transfer_info
+= " [" + friendlyUnit(serverState
.dl_rate_limit
, true) + "]";
609 transfer_info
+= " (" + friendlyUnit(serverState
.dl_info_data
, false) + ")";
610 $("DlInfos").set('html', transfer_info
);
611 transfer_info
= friendlyUnit(serverState
.up_info_speed
, true);
612 if (serverState
.up_rate_limit
> 0)
613 transfer_info
+= " [" + friendlyUnit(serverState
.up_rate_limit
, true) + "]";
614 transfer_info
+= " (" + friendlyUnit(serverState
.up_info_data
, false) + ")";
615 $("UpInfos").set('html', transfer_info
);
617 document
.title
= "QBT_TR([D: %1, U: %2] qBittorrent %3)QBT_TR[CONTEXT=MainWindow]".replace("%1", friendlyUnit(serverState
.dl_info_speed
, true)).replace("%2", friendlyUnit(serverState
.up_info_speed
, true)).replace("%3", qbtVersion());
618 document
.title
+= " QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]";
621 document
.title
= ("qBittorrent " + qbtVersion() + " QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]");
622 $('freeSpaceOnDisk').set('html', 'QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]'.replace("%1", friendlyUnit(serverState
.free_space_on_disk
)));
623 $('DHTNodes').set('html', 'QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]'.replace("%1", serverState
.dht_nodes
));
626 if (document
.getElementById("statisticspage")) {
627 $('AlltimeDL').set('html', friendlyUnit(serverState
.alltime_dl
, false));
628 $('AlltimeUL').set('html', friendlyUnit(serverState
.alltime_ul
, false));
629 $('TotalWastedSession').set('html', friendlyUnit(serverState
.total_wasted_session
, false));
630 $('GlobalRatio').set('html', serverState
.global_ratio
);
631 $('TotalPeerConnections').set('html', serverState
.total_peer_connections
);
632 $('ReadCacheHits').set('html', serverState
.read_cache_hits
+ "%");
633 $('TotalBuffersSize').set('html', friendlyUnit(serverState
.total_buffers_size
, false));
634 $('WriteCacheOverload').set('html', serverState
.write_cache_overload
+ "%");
635 $('ReadCacheOverload').set('html', serverState
.read_cache_overload
+ "%");
636 $('QueuedIOJobs').set('html', serverState
.queued_io_jobs
);
637 $('AverageTimeInQueue').set('html', serverState
.average_time_queue
+ " ms");
638 $('TotalQueuedSize').set('html', friendlyUnit(serverState
.total_queued_size
, false));
641 if (serverState
.connection_status
== "connected")
642 $('connectionStatus').src
= 'images/skin/connected.svg';
643 else if (serverState
.connection_status
== "firewalled")
644 $('connectionStatus').src
= 'images/skin/firewalled.svg';
646 $('connectionStatus').src
= 'images/skin/disconnected.svg';
648 if (queueing_enabled
!= serverState
.queueing
) {
649 queueing_enabled
= serverState
.queueing
;
650 torrentsTable
.columns
['priority'].force_hide
= !queueing_enabled
;
651 torrentsTable
.updateColumn('priority');
652 if (queueing_enabled
) {
653 $('topQueuePosItem').removeClass('invisible');
654 $('increaseQueuePosItem').removeClass('invisible');
655 $('decreaseQueuePosItem').removeClass('invisible');
656 $('bottomQueuePosItem').removeClass('invisible');
657 $('queueingButtons').removeClass('invisible');
658 $('queueingMenuItems').removeClass('invisible');
661 $('topQueuePosItem').addClass('invisible');
662 $('increaseQueuePosItem').addClass('invisible');
663 $('decreaseQueuePosItem').addClass('invisible');
664 $('bottomQueuePosItem').addClass('invisible');
665 $('queueingButtons').addClass('invisible');
666 $('queueingMenuItems').addClass('invisible');
670 if (alternativeSpeedLimits
!= serverState
.use_alt_speed_limits
) {
671 alternativeSpeedLimits
= serverState
.use_alt_speed_limits
;
672 updateAltSpeedIcon(alternativeSpeedLimits
);
675 serverSyncMainDataInterval
= Math
.max(serverState
.refresh_interval
, 500);
678 const updateAltSpeedIcon = function(enabled
) {
680 $('alternativeSpeedLimits').src
= "images/slow.svg";
682 $('alternativeSpeedLimits').src
= "images/slow_off.svg";
685 $('alternativeSpeedLimits').addEvent('click', function() {
686 // Change icon immediately to give some feedback
687 updateAltSpeedIcon(!alternativeSpeedLimits
);
690 url
: 'api/v2/transfer/toggleSpeedLimitsMode',
692 onComplete: function() {
693 alternativeSpeedLimits
= !alternativeSpeedLimits
;
696 onFailure: function() {
697 // Restore icon in case of failure
698 updateAltSpeedIcon(alternativeSpeedLimits
);
703 $('DlInfos').addEvent('click', globalDownloadLimitFN
);
704 $('UpInfos').addEvent('click', globalUploadLimitFN
);
706 $('showTopToolbarLink').addEvent('click', function(e
) {
707 showTopToolbar
= !showTopToolbar
;
708 localStorage
.setItem('show_top_toolbar', showTopToolbar
.toString());
709 if (showTopToolbar
) {
710 $('showTopToolbarLink').firstChild
.style
.opacity
= '1';
711 $('mochaToolbar').removeClass('invisible');
714 $('showTopToolbarLink').firstChild
.style
.opacity
= '0';
715 $('mochaToolbar').addClass('invisible');
717 MochaUI
.Desktop
.setDesktopSize();
720 $('showStatusBarLink').addEvent('click', function(e
) {
721 showStatusBar
= !showStatusBar
;
722 localStorage
.setItem('show_status_bar', showStatusBar
.toString());
724 $('showStatusBarLink').firstChild
.style
.opacity
= '1';
725 $('desktopFooterWrapper').removeClass('invisible');
728 $('showStatusBarLink').firstChild
.style
.opacity
= '0';
729 $('desktopFooterWrapper').addClass('invisible');
731 MochaUI
.Desktop
.setDesktopSize();
734 $('registerMagnetHandlerLink').addEvent('click', function(e
) {
735 registerMagnetHandler();
738 $('speedInBrowserTitleBarLink').addEvent('click', function(e
) {
739 speedInTitle
= !speedInTitle
;
740 localStorage
.setItem('speed_in_browser_title_bar', speedInTitle
.toString());
742 $('speedInBrowserTitleBarLink').firstChild
.style
.opacity
= '1';
744 $('speedInBrowserTitleBarLink').firstChild
.style
.opacity
= '0';
745 processServerState();
748 $('showSearchEngineLink').addEvent('click', function(e
) {
749 showSearchEngine
= !showSearchEngine
;
750 localStorage
.setItem('show_search_engine', showSearchEngine
.toString());
751 if (showSearchEngine
) {
752 $('showSearchEngineLink').firstChild
.style
.opacity
= '1';
753 $('mainWindowTabs').removeClass('invisible');
755 addMainWindowTabsEventListener();
756 if (!MochaUI
.Panels
.instances
.SearchPanel
)
760 $('showSearchEngineLink').firstChild
.style
.opacity
= '0';
761 $('mainWindowTabs').addClass('invisible');
762 $("transfersTabLink").click();
764 removeMainWindowTabsEventListener();
768 $('StatisticsLink').addEvent('click', StatisticsLinkFN
);
772 const showTransfersTab = function() {
773 $("filtersColumn").removeClass("invisible");
774 $("filtersColumn_handle").removeClass("invisible");
775 $("mainColumn").removeClass("invisible");
777 customSyncMainDataInterval
= null;
778 clearTimeout(syncMainDataTimer
);
779 syncMainDataTimer
= syncMainData
.delay(100);
784 const hideTransfersTab = function() {
785 $("filtersColumn").addClass("invisible");
786 $("filtersColumn_handle").addClass("invisible");
787 $("mainColumn").addClass("invisible");
788 MochaUI
.Desktop
.resizePanels();
791 const showSearchTab = function() {
792 if (!searchTabInitialized
) {
794 searchTabInitialized
= true;
797 $("searchTabColumn").removeClass("invisible");
798 customSyncMainDataInterval
= 30000;
802 const hideSearchTab = function() {
803 $("searchTabColumn").addClass("invisible");
804 MochaUI
.Desktop
.resizePanels();
807 const addMainWindowTabsEventListener = function() {
808 $('transfersTabLink').addEvent('click', showTransfersTab
);
809 $('searchTabLink').addEvent('click', showSearchTab
);
812 const removeMainWindowTabsEventListener = function() {
813 $('transfersTabLink').removeEvent('click', showTransfersTab
);
814 $('searchTabLink').removeEvent('click', showSearchTab
);
817 const addSearchPanel = function() {
829 contentURL
: 'views/search.html',
831 column
: 'searchTabColumn',
847 contentURL
: 'views/transferlist.html',
848 onContentLoaded: function() {
849 handleDownloadParam();
852 column
: 'mainColumn',
853 onResize
: saveColumnSizes
,
856 let prop_h
= localStorage
.getItem('properties_height_rel');
857 if ($defined(prop_h
))
858 prop_h
= prop_h
.toFloat() * Window
.getSize().y
;
860 prop_h
= Window
.getSize().y
/ 2.0;
862 id
: 'propertiesPanel',
871 contentURL
: 'views/properties.html',
873 css
: ['css/Tabs.css', 'css/dynamicTable.css'],
874 js
: ['scripts/prop-general.js', 'scripts/prop-trackers.js', 'scripts/prop-peers.js', 'scripts/prop-webseeds.js', 'scripts/prop-files.js'],
876 tabsURL
: 'views/propertiesToolbar.html',
877 tabsOnload: function() {
878 MochaUI
.initializeTabs('propertiesTabs');
880 updatePropertiesPanel = function() {
881 if (!$('prop_general').hasClass('invisible'))
883 else if (!$('prop_trackers').hasClass('invisible'))
884 updateTrackersData();
885 else if (!$('prop_peers').hasClass('invisible'))
886 updateTorrentPeersData();
887 else if (!$('prop_webseeds').hasClass('invisible'))
888 updateWebSeedsData();
889 else if (!$('prop_files').hasClass('invisible'))
890 updateTorrentFilesData();
893 $('PropGeneralLink').addEvent('click', function(e
) {
894 $$('.propertiesTabContent').addClass('invisible');
895 $('prop_general').removeClass("invisible");
897 updatePropertiesPanel();
898 localStorage
.setItem('selected_tab', this.id
);
901 $('PropTrackersLink').addEvent('click', function(e
) {
902 $$('.propertiesTabContent').addClass('invisible');
903 $('prop_trackers').removeClass("invisible");
905 updatePropertiesPanel();
906 localStorage
.setItem('selected_tab', this.id
);
909 $('PropPeersLink').addEvent('click', function(e
) {
910 $$('.propertiesTabContent').addClass('invisible');
911 $('prop_peers').removeClass("invisible");
913 updatePropertiesPanel();
914 localStorage
.setItem('selected_tab', this.id
);
917 $('PropWebSeedsLink').addEvent('click', function(e
) {
918 $$('.propertiesTabContent').addClass('invisible');
919 $('prop_webseeds').removeClass("invisible");
921 updatePropertiesPanel();
922 localStorage
.setItem('selected_tab', this.id
);
925 $('PropFilesLink').addEvent('click', function(e
) {
926 $$('.propertiesTabContent').addClass('invisible');
927 $('prop_files').removeClass("invisible");
929 updatePropertiesPanel();
930 localStorage
.setItem('selected_tab', this.id
);
933 $('propertiesPanel_collapseToggle').addEvent('click', function(e
) {
934 updatePropertiesPanel();
937 column
: 'mainColumn',
941 const showFilesFilter = function() {
942 $('torrentFilesFilterToolbar').removeClass("invisible");
945 const hideFilesFilter = function() {
946 $('torrentFilesFilterToolbar').addClass("invisible");
949 let prevTorrentsFilterValue
;
950 let torrentsFilterInputTimer
= null;
951 // listen for changes to torrentsFilterInput
952 $('torrentsFilterInput').addEvent('input', function() {
953 const value
= $('torrentsFilterInput').get("value");
954 if (value
!== prevTorrentsFilterValue
) {
955 prevTorrentsFilterValue
= value
;
956 clearTimeout(torrentsFilterInputTimer
);
957 torrentsFilterInputTimer
= setTimeout(function() {
958 torrentsTable
.updateTable(false);
963 if (showSearchEngine
) {
964 addMainWindowTabsEventListener();
969 function registerMagnetHandler() {
970 if (typeof navigator
.registerProtocolHandler
!== 'function') {
971 alert("Your browser does not support this feature");
975 const hashParams
= getHashParamsFromUrl();
976 hashParams
.download
= '';
978 const templateHashString
= Object
.toQueryString(hashParams
).replace('download=', 'download=%s');
980 const templateUrl
= location
.origin
+ location
.pathname
981 + location
.search
+ '#' + templateHashString
;
983 navigator
.registerProtocolHandler('magnet', templateUrl
,
984 'qBittorrent WebUI magnet handler');
987 function handleDownloadParam() {
988 // Extract torrent URL from download param in WebUI URL hash
989 const downloadHash
= "#download=";
990 if (location
.hash
.indexOf(downloadHash
) !== 0)
993 const url
= location
.hash
.substring(downloadHash
.length
);
994 // Remove the processed hash from the URL
995 history
.replaceState('', document
.title
, (location
.pathname
+ location
.search
));
996 showDownloadPage([url
]);
999 function getHashParamsFromUrl() {
1000 const hashString
= location
.hash
? location
.hash
.replace(/^#/, '') : '';
1001 return (hashString
.length
> 0) ? String
.parseQueryString(hashString
) : {};
1004 function closeWindows() {
1008 function setupCopyEventHandler() {
1010 clipboardEvent
.destroy();
1012 clipboardEvent
= new ClipboardJS('.copyToClipboard', {
1013 text: function(trigger
) {
1014 switch (trigger
.id
) {
1016 return copyNameFN();
1017 case "copyMagnetLink":
1018 return copyMagnetLinkFN();
1020 return copyHashFN();
1029 defaultEventType
: 'keydown',
1031 'ctrl+a': function(event
) {
1032 torrentsTable
.selectAll();
1033 event
.preventDefault();
1035 'delete': function(event
) {
1037 event
.preventDefault();