Move WebUI views into separate folder
[qBittorrent.git] / src / webui / www / private / scripts / client.js
blob6b9455d28911a0810a8e51e85e496f6a1899fc46
1 /*
2 * MIT License
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
22 * THE SOFTWARE.
25 'use strict';
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;
49 let clipboardEvent;
51 const CATEGORIES_ALL = 1;
52 const CATEGORIES_UNCATEGORIZED = 2;
54 let category_list = {};
56 let selected_category = CATEGORIES_ALL;
57 let setCategoryFilter = function() {};
59 const TAGS_ALL = 1;
60 const TAGS_UNTAGGED = 2;
62 let tagList = {};
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);
79 loadSelectedTag();
81 function genHash(string) {
82 let hash = 0;
83 for (let i = 0; i < string.length; ++i) {
84 const c = string.charCodeAt(i);
85 hash = (c + hash * 31) | 0;
87 return hash;
90 function getSyncMainDataInterval() {
91 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
94 const fetchQbtVersion = function() {
95 new Request({
96 url: 'api/v2/app/version',
97 method: 'get',
98 onSuccess: function(info) {
99 if (!info) return;
100 sessionStorage.setItem('qbtVersion', info);
102 }).send();
104 fetchQbtVersion();
106 const qbtVersion = function() {
107 const version = sessionStorage.getItem('qbtVersion');
108 if (!version)
109 return '';
110 return version;
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'
132 });*/
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();
139 else
140 filt_w = 120;
141 new MochaUI.Column({
142 id: 'filtersColumn',
143 placement: 'left',
144 onResize: saveColumnSizes,
145 width: filt_w,
146 resizeLimit: [1, 300]
149 new MochaUI.Column({
150 id: 'mainColumn',
151 placement: 'main'
155 const buildSearchTab = function() {
156 new MochaUI.Column({
157 id: 'searchTabColumn',
158 placement: 'main',
159 width: null
162 // start off hidden
163 $("searchTabColumn").addClass("invisible");
166 buildTransfersTab();
167 buildSearchTab();
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')
175 updateMainData();
178 setTagFilter = function(hash) {
179 selectedTag = hash.toString();
180 localStorage.setItem('selected_tag', selectedTag);
181 highlightSelectedTag();
182 if (torrentsTable.tableBody !== undefined)
183 updateMainData();
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");
198 selected_filter = f;
199 localStorage.setItem('selected_filter', f);
200 // Reload torrents
201 if (typeof torrentsTable.tableBody != 'undefined')
202 updateMainData();
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");
211 if (toggleIcon)
212 toggleIcon[0].toggleClass("rotate");
215 new MochaUI.Panel({
216 id: 'Filters',
217 title: 'Panel',
218 header: false,
219 padding: {
220 top: 0,
221 right: 0,
222 bottom: 0,
223 left: 0
225 loadMethod: 'xhr',
226 contentURL: 'views/filters.html',
227 onContentLoaded: function() {
228 setFilter(selected_filter);
230 column: 'filtersColumn',
231 height: 300
233 initializeWindows();
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";
254 if (!speedInTitle)
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';
262 // hide tabs
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 === "")
274 return false;
275 let removed = false;
276 Object.each(category_list, function(category) {
277 if (Object.contains(category.torrents, hash)) {
278 removed = true;
279 category.torrents.splice(category.torrents.indexOf(hash), 1);
282 return removed;
285 const addTorrentToCategoryList = function(torrent) {
286 const category = torrent['category'];
287 if (typeof category === 'undefined')
288 return false;
289 if (category.length === 0) { // Empty category
290 removeTorrentFromCategoryList(torrent['hash']);
291 return true;
293 const categoryHash = genHash(category);
294 if (category_list[categoryHash] === null) // This should not happen
295 category_list[categoryHash] = {
296 name: category,
297 torrents: []
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']]);
302 return true;
304 return false;
307 const removeTorrentFromTagList = function(hash) {
308 if ((hash === null) || (hash === ""))
309 return false;
311 let removed = false;
312 for (const key in tagList) {
313 const tag = tagList[key];
314 if (Object.contains(tag.torrents, hash)) {
315 removed = true;
316 tag.torrents.splice(tag.torrents.indexOf(hash), 1);
319 return removed;
322 const addTorrentToTagList = function(torrent) {
323 if (torrent['tags'] === undefined) // Tags haven't changed
324 return false;
326 removeTorrentFromTagList(torrent['hash']);
328 if (torrent['tags'].length === 0) // No tags
329 return true;
331 const tags = torrent['tags'].split(',');
332 let added = false;
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'])) {
336 added = true;
337 tagList[tagHash].torrents.push(torrent['hash']);
340 return added;
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');
361 if (!categoryList)
362 return;
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', {
370 id: hash,
371 html: html
373 categoriesFilterContextMenu.addTarget(el);
374 return 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)
381 uncategorized += 1;
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');
403 if (!categoryList)
404 return;
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";
409 else
410 children[i].className = "";
414 const updateTagList = function() {
415 const tagFilterList = $('tagFilterList');
416 if (tagFilterList === null)
417 return;
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', {
427 id: hash,
428 html: html
430 tagsFilterContextMenu.addTarget(el);
431 return el;
434 const torrentsCount = torrentsTable.getRowIds().length;
435 let untagged = 0;
436 for (const key in torrentsTable.rows) {
437 if (torrentsTable.rows.hasOwnProperty(key) && torrentsTable.rows[key]['full_data'].tags.length === 0)
438 untagged += 1;
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);
446 sortedTags.sort();
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');
460 if (!tagFilterList)
461 return;
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);
472 new Request.JSON({
473 url: url,
474 noCache: true,
475 method: 'get',
476 onFailure: function() {
477 const errorDiv = $('error_div');
478 if (errorDiv)
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', '');
485 if (response) {
486 clearTimeout(torrentsFilterInputTimer);
487 let torrentsTableSelectedRows;
488 let update_categories = false;
489 let updateTags = false;
490 const full_update = (response['full_update'] === true);
491 if (full_update) {
492 torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
493 torrentsTable.clear();
494 category_list = {};
495 tagList = {};
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;
508 else {
509 category_list[categoryHash] = {
510 name: category.name,
511 savePath: category.savePath,
512 torrents: []
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]) {
529 tagList[tagHash] = {
530 name: tag,
531 torrents: []
535 updateTags = true;
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];
542 updateTags = true;
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]))
555 updateTags = true;
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'];
575 for (const k in tmp)
576 serverState[k] = tmp[k];
577 processServerState();
579 updateFiltersList();
580 if (update_categories) {
581 updateCategoryList();
582 torrentsTableContextMenu.updateCategoriesSubMenu(category_list);
584 if (updateTags) {
585 updateTagList();
586 torrentsTableContextMenu.updateTagsSubMenu(tagList);
589 if (full_update)
590 // re-select previously selected rows
591 torrentsTable.reselectRows(torrentsTableSelectedRows);
593 clearTimeout(syncMainDataTimer);
594 syncMainDataTimer = syncMainData.delay(getSyncMainDataInterval());
596 }).send();
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);
616 if (speedInTitle) {
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]";
620 else
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));
625 // Statistics dialog
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';
645 else
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');
660 else {
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) {
679 if (enabled)
680 $('alternativeSpeedLimits').src = "images/slow.svg";
681 else
682 $('alternativeSpeedLimits').src = "images/slow_off.svg";
685 $('alternativeSpeedLimits').addEvent('click', function() {
686 // Change icon immediately to give some feedback
687 updateAltSpeedIcon(!alternativeSpeedLimits);
689 new Request({
690 url: 'api/v2/transfer/toggleSpeedLimitsMode',
691 method: 'post',
692 onComplete: function() {
693 alternativeSpeedLimits = !alternativeSpeedLimits;
694 updateMainData();
696 onFailure: function() {
697 // Restore icon in case of failure
698 updateAltSpeedIcon(alternativeSpeedLimits);
700 }).send();
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');
713 else {
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());
723 if (showStatusBar) {
724 $('showStatusBarLink').firstChild.style.opacity = '1';
725 $('desktopFooterWrapper').removeClass('invisible');
727 else {
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());
741 if (speedInTitle)
742 $('speedInBrowserTitleBarLink').firstChild.style.opacity = '1';
743 else
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)
757 addSearchPanel();
759 else {
760 $('showSearchEngineLink').firstChild.style.opacity = '0';
761 $('mainWindowTabs').addClass('invisible');
762 $("transfersTabLink").click();
764 removeMainWindowTabsEventListener();
768 $('StatisticsLink').addEvent('click', StatisticsLinkFN);
770 // main window tabs
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);
781 hideSearchTab();
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) {
793 initSearchTab();
794 searchTabInitialized = true;
797 $("searchTabColumn").removeClass("invisible");
798 customSyncMainDataInterval = 30000;
799 hideTransfersTab();
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() {
818 new MochaUI.Panel({
819 id: 'SearchPanel',
820 title: 'Search',
821 header: false,
822 padding: {
823 top: 0,
824 right: 0,
825 bottom: 0,
826 left: 0
828 loadMethod: 'xhr',
829 contentURL: 'views/search.html',
830 content: '',
831 column: 'searchTabColumn',
832 height: null
836 new MochaUI.Panel({
837 id: 'transferList',
838 title: 'Panel',
839 header: false,
840 padding: {
841 top: 0,
842 right: 0,
843 bottom: 0,
844 left: 0
846 loadMethod: 'xhr',
847 contentURL: 'views/transferlist.html',
848 onContentLoaded: function() {
849 handleDownloadParam();
850 updateMainData();
852 column: 'mainColumn',
853 onResize: saveColumnSizes,
854 height: null
856 let prop_h = localStorage.getItem('properties_height_rel');
857 if ($defined(prop_h))
858 prop_h = prop_h.toFloat() * Window.getSize().y;
859 else
860 prop_h = Window.getSize().y / 2.0;
861 new MochaUI.Panel({
862 id: 'propertiesPanel',
863 title: 'Panel',
864 header: true,
865 padding: {
866 top: 0,
867 right: 0,
868 bottom: 0,
869 left: 0
871 contentURL: 'views/properties.html',
872 require: {
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'))
882 updateTorrentData();
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");
896 hideFilesFilter();
897 updatePropertiesPanel();
898 localStorage.setItem('selected_tab', this.id);
901 $('PropTrackersLink').addEvent('click', function(e) {
902 $$('.propertiesTabContent').addClass('invisible');
903 $('prop_trackers').removeClass("invisible");
904 hideFilesFilter();
905 updatePropertiesPanel();
906 localStorage.setItem('selected_tab', this.id);
909 $('PropPeersLink').addEvent('click', function(e) {
910 $$('.propertiesTabContent').addClass('invisible');
911 $('prop_peers').removeClass("invisible");
912 hideFilesFilter();
913 updatePropertiesPanel();
914 localStorage.setItem('selected_tab', this.id);
917 $('PropWebSeedsLink').addEvent('click', function(e) {
918 $$('.propertiesTabContent').addClass('invisible');
919 $('prop_webseeds').removeClass("invisible");
920 hideFilesFilter();
921 updatePropertiesPanel();
922 localStorage.setItem('selected_tab', this.id);
925 $('PropFilesLink').addEvent('click', function(e) {
926 $$('.propertiesTabContent').addClass('invisible');
927 $('prop_files').removeClass("invisible");
928 showFilesFilter();
929 updatePropertiesPanel();
930 localStorage.setItem('selected_tab', this.id);
933 $('propertiesPanel_collapseToggle').addEvent('click', function(e) {
934 updatePropertiesPanel();
937 column: 'mainColumn',
938 height: prop_h
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);
959 }, 400);
963 if (showSearchEngine) {
964 addMainWindowTabsEventListener();
965 addSearchPanel();
969 function registerMagnetHandler() {
970 if (typeof navigator.registerProtocolHandler !== 'function') {
971 alert("Your browser does not support this feature");
972 return;
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)
991 return;
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() {
1005 MochaUI.closeAll();
1008 function setupCopyEventHandler() {
1009 if (clipboardEvent)
1010 clipboardEvent.destroy();
1012 clipboardEvent = new ClipboardJS('.copyToClipboard', {
1013 text: function(trigger) {
1014 switch (trigger.id) {
1015 case "copyName":
1016 return copyNameFN();
1017 case "copyMagnetLink":
1018 return copyMagnetLinkFN();
1019 case "copyHash":
1020 return copyHashFN();
1021 default:
1022 return "";
1028 new Keyboard({
1029 defaultEventType: 'keydown',
1030 events: {
1031 'ctrl+a': function(event) {
1032 torrentsTable.selectAll();
1033 event.preventDefault();
1035 'delete': function(event) {
1036 deleteFN();
1037 event.preventDefault();
1040 }).activate();