WebUI: Add ability to toggle alternating row colors in tables
[qBittorrent.git] / src / webui / www / private / scripts / client.js
blobcd24d3612624946a9f8e055be14516cca24bac85
1 /*
2 * MIT License
3 * Copyright (C) 2024 Mike Tzou (Chocobo1)
4 * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>,
5 * Christophe Dumez <chris@qbittorrent.org>
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
26 "use strict";
28 window.qBittorrent ??= {};
29 window.qBittorrent.Client ??= (() => {
30 const exports = () => {
31 return {
32 closeWindows: closeWindows,
33 genHash: genHash,
34 getSyncMainDataInterval: getSyncMainDataInterval,
35 isStopped: isStopped,
36 stop: stop,
37 mainTitle: mainTitle,
38 showSearchEngine: showSearchEngine,
39 showRssReader: showRssReader,
40 showLogViewer: showLogViewer,
41 isShowSearchEngine: isShowSearchEngine,
42 isShowRssReader: isShowRssReader,
43 isShowLogViewer: isShowLogViewer
47 const closeWindows = function() {
48 MochaUI.closeAll();
51 const genHash = function(string) {
52 // origins:
53 // https://stackoverflow.com/a/8831937
54 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
55 let hash = 0;
56 for (let i = 0; i < string.length; ++i)
57 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
58 return hash;
61 const getSyncMainDataInterval = function() {
62 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
65 let stopped = false;
66 const isStopped = () => {
67 return stopped;
70 const stop = () => {
71 stopped = true;
74 const mainTitle = () => {
75 const emDash = "\u2014";
76 const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
77 const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
78 const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
79 + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
80 return title;
83 let showingSearchEngine = false;
84 let showingRssReader = false;
85 let showingLogViewer = false;
87 const showSearchEngine = function(bool) {
88 showingSearchEngine = bool;
90 const showRssReader = function(bool) {
91 showingRssReader = bool;
93 const showLogViewer = function(bool) {
94 showingLogViewer = bool;
96 const isShowSearchEngine = function() {
97 return showingSearchEngine;
99 const isShowRssReader = function() {
100 return showingRssReader;
102 const isShowLogViewer = function() {
103 return showingLogViewer;
106 return exports();
107 })();
108 Object.freeze(window.qBittorrent.Client);
110 // TODO: move global functions/variables into some namespace/scope
112 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
114 let updatePropertiesPanel = function() {};
116 this.updateMainData = function() {};
117 let alternativeSpeedLimits = false;
118 let queueing_enabled = true;
119 let serverSyncMainDataInterval = 1500;
120 let customSyncMainDataInterval = null;
121 let useSubcategories = true;
123 /* Categories filter */
124 const CATEGORIES_ALL = 1;
125 const CATEGORIES_UNCATEGORIZED = 2;
127 const category_list = new Map();
129 let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
130 let setCategoryFilter = function() {};
132 /* Tags filter */
133 const TAGS_ALL = 1;
134 const TAGS_UNTAGGED = 2;
136 const tagList = new Map();
138 let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
139 let setTagFilter = function() {};
141 /* Trackers filter */
142 const TRACKERS_ALL = 1;
143 const TRACKERS_TRACKERLESS = 2;
145 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
146 const trackerList = new Map();
148 let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
149 let setTrackerFilter = function() {};
151 /* All filters */
152 let selected_filter = LocalPreferences.get("selected_filter", "all");
153 let setFilter = function() {};
154 let toggleFilterDisplay = function() {};
156 window.addEventListener("DOMContentLoaded", () => {
157 let isSearchPanelLoaded = false;
158 let isLogPanelLoaded = false;
160 const saveColumnSizes = function() {
161 const filters_width = $("Filters").getSize().x;
162 LocalPreferences.set("filters_width", filters_width);
163 const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
164 LocalPreferences.set("properties_height_rel", properties_height_rel);
167 window.addEvent("resize", () => {
168 // only save sizes if the columns are visible
169 if (!$("mainColumn").hasClass("invisible"))
170 saveColumnSizes.delay(200); // Resizing might takes some time.
173 /* MochaUI.Desktop = new MochaUI.Desktop();
174 MochaUI.Desktop.desktop.setStyles({
175 'background': '#fff',
176 'visibility': 'visible'
177 });*/
178 MochaUI.Desktop.initialize();
180 const buildTransfersTab = function() {
181 const filt_w = Number(LocalPreferences.get("filters_width", 120));
182 new MochaUI.Column({
183 id: "filtersColumn",
184 placement: "left",
185 onResize: saveColumnSizes,
186 width: filt_w,
187 resizeLimit: [1, 300]
189 new MochaUI.Column({
190 id: "mainColumn",
191 placement: "main"
195 const buildSearchTab = function() {
196 new MochaUI.Column({
197 id: "searchTabColumn",
198 placement: "main",
199 width: null
202 // start off hidden
203 $("searchTabColumn").addClass("invisible");
206 const buildRssTab = function() {
207 new MochaUI.Column({
208 id: "rssTabColumn",
209 placement: "main",
210 width: null
213 // start off hidden
214 $("rssTabColumn").addClass("invisible");
217 const buildLogTab = function() {
218 new MochaUI.Column({
219 id: "logTabColumn",
220 placement: "main",
221 width: null
224 // start off hidden
225 $("logTabColumn").addClass("invisible");
228 buildTransfersTab();
229 buildSearchTab();
230 buildRssTab();
231 buildLogTab();
232 MochaUI.initializeTabs("mainWindowTabsList");
234 setCategoryFilter = function(hash) {
235 selected_category = hash;
236 LocalPreferences.set("selected_category", selected_category);
237 highlightSelectedCategory();
238 if (typeof torrentsTable.tableBody !== "undefined")
239 updateMainData();
242 setTagFilter = function(hash) {
243 selectedTag = hash;
244 LocalPreferences.set("selected_tag", selectedTag);
245 highlightSelectedTag();
246 if (torrentsTable.tableBody !== undefined)
247 updateMainData();
250 setTrackerFilter = function(hash) {
251 selectedTracker = hash.toString();
252 LocalPreferences.set("selected_tracker", selectedTracker);
253 highlightSelectedTracker();
254 if (torrentsTable.tableBody !== undefined)
255 updateMainData();
258 setFilter = function(f) {
259 // Visually Select the right filter
260 $("all_filter").removeClass("selectedFilter");
261 $("downloading_filter").removeClass("selectedFilter");
262 $("seeding_filter").removeClass("selectedFilter");
263 $("completed_filter").removeClass("selectedFilter");
264 $("stopped_filter").removeClass("selectedFilter");
265 $("running_filter").removeClass("selectedFilter");
266 $("active_filter").removeClass("selectedFilter");
267 $("inactive_filter").removeClass("selectedFilter");
268 $("stalled_filter").removeClass("selectedFilter");
269 $("stalled_uploading_filter").removeClass("selectedFilter");
270 $("stalled_downloading_filter").removeClass("selectedFilter");
271 $("checking_filter").removeClass("selectedFilter");
272 $("moving_filter").removeClass("selectedFilter");
273 $("errored_filter").removeClass("selectedFilter");
274 $(f + "_filter").addClass("selectedFilter");
275 selected_filter = f;
276 LocalPreferences.set("selected_filter", f);
277 // Reload torrents
278 if (typeof torrentsTable.tableBody !== "undefined")
279 updateMainData();
282 toggleFilterDisplay = function(filter) {
283 const element = filter + "FilterList";
284 LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
285 $(element).toggleClass("invisible");
286 const parent = $(element).getParent(".filterWrapper");
287 const toggleIcon = $(parent).getChildren(".filterTitle img");
288 if (toggleIcon)
289 toggleIcon[0].toggleClass("rotate");
292 new MochaUI.Panel({
293 id: "Filters",
294 title: "Panel",
295 header: false,
296 padding: {
297 top: 0,
298 right: 0,
299 bottom: 0,
300 left: 0
302 loadMethod: "xhr",
303 contentURL: "views/filters.html",
304 onContentLoaded: function() {
305 setFilter(selected_filter);
307 column: "filtersColumn",
308 height: 300
310 initializeWindows();
312 // Show Top Toolbar is enabled by default
313 let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
314 if (!showTopToolbar) {
315 $("showTopToolbarLink").firstChild.style.opacity = "0";
316 $("mochaToolbar").addClass("invisible");
319 // Show Status Bar is enabled by default
320 let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
321 if (!showStatusBar) {
322 $("showStatusBarLink").firstChild.style.opacity = "0";
323 $("desktopFooterWrapper").addClass("invisible");
326 // Show Filters Sidebar is enabled by default
327 let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
328 if (!showFiltersSidebar) {
329 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
330 $("filtersColumn").addClass("invisible");
331 $("filtersColumn_handle").addClass("invisible");
334 let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
335 if (!speedInTitle)
336 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
338 // After showing/hiding the toolbar + status bar
339 window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
340 window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
341 window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
343 // After Show Top Toolbar
344 MochaUI.Desktop.setDesktopSize();
346 let syncMainDataLastResponseId = 0;
347 const serverState = {};
349 const removeTorrentFromCategoryList = function(hash) {
350 if (!hash)
351 return false;
353 let removed = false;
354 category_list.forEach((category) => {
355 const deleteResult = category.torrents.delete(hash);
356 removed ||= deleteResult;
359 return removed;
362 const addTorrentToCategoryList = function(torrent) {
363 const category = torrent["category"];
364 if (typeof category === "undefined")
365 return false;
367 const hash = torrent["hash"];
368 if (category.length === 0) { // Empty category
369 removeTorrentFromCategoryList(hash);
370 return true;
373 const categoryHash = window.qBittorrent.Client.genHash(category);
374 if (!category_list.has(categoryHash)) { // This should not happen
375 category_list.set(categoryHash, {
376 name: category,
377 torrents: new Set()
381 const torrents = category_list.get(categoryHash).torrents;
382 if (!torrents.has(hash)) {
383 removeTorrentFromCategoryList(hash);
384 torrents.add(hash);
385 return true;
387 return false;
390 const removeTorrentFromTagList = function(hash) {
391 if (!hash)
392 return false;
394 let removed = false;
395 tagList.forEach((tag) => {
396 const deleteResult = tag.torrents.delete(hash);
397 removed ||= deleteResult;
400 return removed;
403 const addTorrentToTagList = function(torrent) {
404 if (torrent["tags"] === undefined) // Tags haven't changed
405 return false;
407 const hash = torrent["hash"];
408 removeTorrentFromTagList(hash);
410 if (torrent["tags"].length === 0) // No tags
411 return true;
413 const tags = torrent["tags"].split(",");
414 let added = false;
415 for (let i = 0; i < tags.length; ++i) {
416 const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
417 if (!tagList.has(tagHash)) { // This should not happen
418 tagList.set(tagHash, {
419 name: tags,
420 torrents: new Set()
424 const torrents = tagList.get(tagHash).torrents;
425 if (!torrents.has(hash)) {
426 torrents.add(hash);
427 added = true;
430 return added;
433 const updateFilter = function(filter, filterTitle) {
434 $(filter + "_filter").firstChild.childNodes[1].nodeValue = filterTitle.replace("%1", torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL));
437 const updateFiltersList = function() {
438 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
439 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
440 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
441 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
442 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
443 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
444 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
454 const updateCategoryList = function() {
455 const categoryList = $("categoryFilterList");
456 if (!categoryList)
457 return;
458 categoryList.getChildren().each(c => c.destroy());
460 const create_link = function(hash, text, count) {
461 let display_name = text;
462 let margin_left = 0;
463 if (useSubcategories) {
464 const category_path = text.split("/");
465 display_name = category_path[category_path.length - 1];
466 margin_left = (category_path.length - 1) * 20;
469 const html = `<span class="link" href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
470 + '<img src="images/view-categories.svg"/>'
471 + window.qBittorrent.Misc.escapeHtml(display_name) + " (" + count + ")" + "</span>";
472 const el = new Element("li", {
473 id: hash,
474 html: html
476 window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
477 return el;
480 const all = torrentsTable.getRowIds().length;
481 let uncategorized = 0;
482 for (const key in torrentsTable.rows) {
483 if (!Object.hasOwn(torrentsTable.rows, key))
484 continue;
486 const row = torrentsTable.rows[key];
487 if (row["full_data"].category.length === 0)
488 uncategorized += 1;
490 categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
491 categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
493 const sortedCategories = [];
494 category_list.forEach((category, hash) => sortedCategories.push({
495 categoryName: category.name,
496 categoryHash: hash,
497 categoryCount: category.torrents.size
498 }));
499 sortedCategories.sort((left, right) => {
500 const leftSegments = left.categoryName.split("/");
501 const rightSegments = right.categoryName.split("/");
503 for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
504 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
505 leftSegments[i], rightSegments[i]);
506 if (compareResult !== 0)
507 return compareResult;
510 return leftSegments.length - rightSegments.length;
513 for (let i = 0; i < sortedCategories.length; ++i) {
514 const { categoryName, categoryHash } = sortedCategories[i];
515 let { categoryCount } = sortedCategories[i];
517 if (useSubcategories) {
518 for (let j = (i + 1);
519 ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
520 categoryCount += sortedCategories[j].categoryCount;
523 categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
526 highlightSelectedCategory();
529 const highlightSelectedCategory = function() {
530 const categoryList = $("categoryFilterList");
531 if (!categoryList)
532 return;
533 const children = categoryList.childNodes;
534 for (let i = 0; i < children.length; ++i) {
535 if (Number(children[i].id) === selected_category)
536 children[i].className = "selectedFilter";
537 else
538 children[i].className = "";
542 const updateTagList = function() {
543 const tagFilterList = $("tagFilterList");
544 if (tagFilterList === null)
545 return;
547 tagFilterList.getChildren().each(c => c.destroy());
549 const createLink = function(hash, text, count) {
550 const html = `<span class="link" href="#" onclick="setTagFilter(${hash}); return false;">`
551 + '<img src="images/tags.svg"/>'
552 + window.qBittorrent.Misc.escapeHtml(text) + " (" + count + ")" + "</span>";
553 const el = new Element("li", {
554 id: hash,
555 html: html
557 window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
558 return el;
561 const torrentsCount = torrentsTable.getRowIds().length;
562 let untagged = 0;
563 for (const key in torrentsTable.rows) {
564 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].tags.length === 0))
565 untagged += 1;
567 tagFilterList.appendChild(createLink(TAGS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount));
568 tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
570 const sortedTags = [];
571 tagList.forEach((tag, hash) => sortedTags.push({
572 tagName: tag.name,
573 tagHash: hash,
574 tagSize: tag.torrents.size
575 }));
576 sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
578 for (const { tagName, tagHash, tagSize } of sortedTags)
579 tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
581 highlightSelectedTag();
584 const highlightSelectedTag = function() {
585 const tagFilterList = $("tagFilterList");
586 if (!tagFilterList)
587 return;
589 const children = tagFilterList.childNodes;
590 for (let i = 0; i < children.length; ++i)
591 children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
594 // getHost emulate the GUI version `QString getHost(const QString &url)`
595 const getHost = function(url) {
596 // We want the hostname.
597 // If failed to parse the domain, original input should be returned
599 if (!/^(?:https?|udp):/i.test(url))
600 return url;
602 try {
603 // hack: URL can not get hostname from udp protocol
604 const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
605 // host: "example.com:8443"
606 // hostname: "example.com"
607 const host = parsedUrl.hostname;
608 if (!host)
609 return url;
611 return host;
613 catch (error) {
614 return url;
618 const updateTrackerList = function() {
619 const trackerFilterList = $("trackerFilterList");
620 if (trackerFilterList === null)
621 return;
623 trackerFilterList.getChildren().each(c => c.destroy());
625 const createLink = function(hash, text, count) {
626 const html = '<span class="link" href="#" onclick="setTrackerFilter(' + hash + ');return false;">'
627 + '<img src="images/trackers.svg"/>'
628 + window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + "</span>";
629 const el = new Element("li", {
630 id: hash,
631 html: html
633 window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
634 return el;
637 const torrentsCount = torrentsTable.getRowIds().length;
638 trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
639 let trackerlessTorrentsCount = 0;
640 for (const key in torrentsTable.rows) {
641 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
642 trackerlessTorrentsCount += 1;
644 trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
646 // Sort trackers by hostname
647 const sortedList = [];
648 trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
649 const uniqueTorrents = new Set();
650 for (const torrents of trackerTorrentMap.values()) {
651 for (const torrent of torrents)
652 uniqueTorrents.add(torrent);
655 sortedList.push({
656 trackerHost: host,
657 trackerHash: hash,
658 trackerCount: uniqueTorrents.size,
661 sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
662 for (const { trackerHost, trackerHash, trackerCount } of sortedList)
663 trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
665 highlightSelectedTracker();
668 const highlightSelectedTracker = function() {
669 const trackerFilterList = $("trackerFilterList");
670 if (!trackerFilterList)
671 return;
673 const children = trackerFilterList.childNodes;
674 for (const child of children)
675 child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
678 const setupCopyEventHandler = (function() {
679 let clipboardEvent;
681 return () => {
682 if (clipboardEvent)
683 clipboardEvent.destroy();
685 clipboardEvent = new ClipboardJS(".copyToClipboard", {
686 text: function(trigger) {
687 switch (trigger.id) {
688 case "copyName":
689 return copyNameFN();
690 case "copyInfohash1":
691 return copyInfohashFN(1);
692 case "copyInfohash2":
693 return copyInfohashFN(2);
694 case "copyMagnetLink":
695 return copyMagnetLinkFN();
696 case "copyID":
697 return copyIdFN();
698 case "copyComment":
699 return copyCommentFN();
700 default:
701 return "";
706 })();
708 let syncMainDataTimeoutID;
709 let syncRequestInProgress = false;
710 const syncMainData = function() {
711 const url = new URI("api/v2/sync/maindata");
712 url.setData("rid", syncMainDataLastResponseId);
713 const request = new Request.JSON({
714 url: url,
715 noCache: true,
716 method: "get",
717 onFailure: function() {
718 const errorDiv = $("error_div");
719 if (errorDiv)
720 errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
721 syncRequestInProgress = false;
722 syncData(2000);
724 onSuccess: function(response) {
725 $("error_div").textContent = "";
726 if (response) {
727 clearTimeout(torrentsFilterInputTimer);
728 torrentsFilterInputTimer = -1;
730 let torrentsTableSelectedRows;
731 let update_categories = false;
732 let updateTags = false;
733 let updateTrackers = false;
734 const full_update = (response["full_update"] === true);
735 if (full_update) {
736 torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
737 torrentsTable.clear();
738 category_list.clear();
739 tagList.clear();
741 if (response["rid"])
742 syncMainDataLastResponseId = response["rid"];
743 if (response["categories"]) {
744 for (const key in response["categories"]) {
745 if (!Object.hasOwn(response["categories"], key))
746 continue;
748 const responseCategory = response["categories"][key];
749 const categoryHash = window.qBittorrent.Client.genHash(key);
750 const category = category_list.get(categoryHash);
751 if (category !== undefined) {
752 // only the save path can change for existing categories
753 category.savePath = responseCategory.savePath;
755 else {
756 category_list.set(categoryHash, {
757 name: responseCategory.name,
758 savePath: responseCategory.savePath,
759 torrents: new Set()
763 update_categories = true;
765 if (response["categories_removed"]) {
766 response["categories_removed"].each((category) => {
767 const categoryHash = window.qBittorrent.Client.genHash(category);
768 category_list.delete(categoryHash);
770 update_categories = true;
772 if (response["tags"]) {
773 for (const tag of response["tags"]) {
774 const tagHash = window.qBittorrent.Client.genHash(tag);
775 if (!tagList.has(tagHash)) {
776 tagList.set(tagHash, {
777 name: tag,
778 torrents: new Set()
782 updateTags = true;
784 if (response["tags_removed"]) {
785 for (let i = 0; i < response["tags_removed"].length; ++i) {
786 const tagHash = window.qBittorrent.Client.genHash(response["tags_removed"][i]);
787 tagList.delete(tagHash);
789 updateTags = true;
791 if (response["trackers"]) {
792 for (const [tracker, torrents] of Object.entries(response["trackers"])) {
793 const host = getHost(tracker);
794 const hash = window.qBittorrent.Client.genHash(host);
796 let trackerListItem = trackerList.get(hash);
797 if (trackerListItem === undefined) {
798 trackerListItem = { host: host, trackerTorrentMap: new Map() };
799 trackerList.set(hash, trackerListItem);
802 trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
804 updateTrackers = true;
806 if (response["trackers_removed"]) {
807 for (let i = 0; i < response["trackers_removed"].length; ++i) {
808 const tracker = response["trackers_removed"][i];
809 const hash = window.qBittorrent.Client.genHash(getHost(tracker));
810 const trackerListEntry = trackerList.get(hash);
811 if (trackerListEntry)
812 trackerListEntry.trackerTorrentMap.delete(tracker);
814 updateTrackers = true;
816 if (response["torrents"]) {
817 let updateTorrentList = false;
818 for (const key in response["torrents"]) {
819 if (!Object.hasOwn(response["torrents"], key))
820 continue;
822 response["torrents"][key]["hash"] = key;
823 response["torrents"][key]["rowId"] = key;
824 if (response["torrents"][key]["state"])
825 response["torrents"][key]["status"] = response["torrents"][key]["state"];
826 torrentsTable.updateRowData(response["torrents"][key]);
827 if (addTorrentToCategoryList(response["torrents"][key]))
828 update_categories = true;
829 if (addTorrentToTagList(response["torrents"][key]))
830 updateTags = true;
831 if (response["torrents"][key]["name"])
832 updateTorrentList = true;
835 if (updateTorrentList)
836 setupCopyEventHandler();
838 if (response["torrents_removed"]) {
839 response["torrents_removed"].each((hash) => {
840 torrentsTable.removeRow(hash);
841 removeTorrentFromCategoryList(hash);
842 update_categories = true; // Always to update All category
843 removeTorrentFromTagList(hash);
844 updateTags = true; // Always to update All tag
847 torrentsTable.updateTable(full_update);
848 if (response["server_state"]) {
849 const tmp = response["server_state"];
850 for (const k in tmp) {
851 if (!Object.hasOwn(tmp, k))
852 continue;
853 serverState[k] = tmp[k];
855 processServerState();
857 updateFiltersList();
858 if (update_categories) {
859 updateCategoryList();
860 window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
862 if (updateTags) {
863 updateTagList();
864 window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
866 if (updateTrackers)
867 updateTrackerList();
869 if (full_update)
870 // re-select previously selected rows
871 torrentsTable.reselectRows(torrentsTableSelectedRows);
873 syncRequestInProgress = false;
874 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
877 syncRequestInProgress = true;
878 request.send();
881 updateMainData = function() {
882 torrentsTable.updateTable();
883 syncData(100);
886 const syncData = function(delay) {
887 if (syncRequestInProgress)
888 return;
890 clearTimeout(syncMainDataTimeoutID);
892 if (window.qBittorrent.Client.isStopped())
893 return;
895 syncMainDataTimeoutID = syncMainData.delay(delay);
898 const processServerState = function() {
899 let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
900 if (serverState.dl_rate_limit > 0)
901 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
902 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
903 $("DlInfos").textContent = transfer_info;
904 transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
905 if (serverState.up_rate_limit > 0)
906 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
907 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
908 $("UpInfos").textContent = transfer_info;
910 document.title = (speedInTitle
911 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
912 .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
913 .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
914 : "")
915 + window.qBittorrent.Client.mainTitle();
917 $("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk));
918 $("DHTNodes").textContent = "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes);
920 // Statistics dialog
921 if (document.getElementById("statisticsContent")) {
922 $("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false);
923 $("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false);
924 $("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false);
925 $("GlobalRatio").textContent = serverState.global_ratio;
926 $("TotalPeerConnections").textContent = serverState.total_peer_connections;
927 $("ReadCacheHits").textContent = serverState.read_cache_hits + "%";
928 $("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false);
929 $("WriteCacheOverload").textContent = serverState.write_cache_overload + "%";
930 $("ReadCacheOverload").textContent = serverState.read_cache_overload + "%";
931 $("QueuedIOJobs").textContent = serverState.queued_io_jobs;
932 $("AverageTimeInQueue").textContent = serverState.average_time_queue + " ms";
933 $("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false);
936 switch (serverState.connection_status) {
937 case "connected":
938 $("connectionStatus").src = "images/connected.svg";
939 $("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
940 $("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
941 break;
942 case "firewalled":
943 $("connectionStatus").src = "images/firewalled.svg";
944 $("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
945 $("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
946 break;
947 default:
948 $("connectionStatus").src = "images/disconnected.svg";
949 $("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
950 $("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
951 break;
954 if (queueing_enabled !== serverState.queueing) {
955 queueing_enabled = serverState.queueing;
956 torrentsTable.columns["priority"].force_hide = !queueing_enabled;
957 torrentsTable.updateColumn("priority");
958 if (queueing_enabled) {
959 $("topQueuePosItem").removeClass("invisible");
960 $("increaseQueuePosItem").removeClass("invisible");
961 $("decreaseQueuePosItem").removeClass("invisible");
962 $("bottomQueuePosItem").removeClass("invisible");
963 $("queueingButtons").removeClass("invisible");
964 $("queueingMenuItems").removeClass("invisible");
966 else {
967 $("topQueuePosItem").addClass("invisible");
968 $("increaseQueuePosItem").addClass("invisible");
969 $("decreaseQueuePosItem").addClass("invisible");
970 $("bottomQueuePosItem").addClass("invisible");
971 $("queueingButtons").addClass("invisible");
972 $("queueingMenuItems").addClass("invisible");
976 if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
977 alternativeSpeedLimits = serverState.use_alt_speed_limits;
978 updateAltSpeedIcon(alternativeSpeedLimits);
981 if (useSubcategories !== serverState.use_subcategories) {
982 useSubcategories = serverState.use_subcategories;
983 updateCategoryList();
986 serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
989 const updateAltSpeedIcon = function(enabled) {
990 if (enabled) {
991 $("alternativeSpeedLimits").src = "images/slow.svg";
992 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
993 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
995 else {
996 $("alternativeSpeedLimits").src = "images/slow_off.svg";
997 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
998 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1002 $("alternativeSpeedLimits").addEvent("click", () => {
1003 // Change icon immediately to give some feedback
1004 updateAltSpeedIcon(!alternativeSpeedLimits);
1006 new Request({
1007 url: "api/v2/transfer/toggleSpeedLimitsMode",
1008 method: "post",
1009 onComplete: function() {
1010 alternativeSpeedLimits = !alternativeSpeedLimits;
1011 updateMainData();
1013 onFailure: function() {
1014 // Restore icon in case of failure
1015 updateAltSpeedIcon(alternativeSpeedLimits);
1017 }).send();
1020 $("DlInfos").addEvent("click", globalDownloadLimitFN);
1021 $("UpInfos").addEvent("click", globalUploadLimitFN);
1023 $("showTopToolbarLink").addEvent("click", (e) => {
1024 showTopToolbar = !showTopToolbar;
1025 LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
1026 if (showTopToolbar) {
1027 $("showTopToolbarLink").firstChild.style.opacity = "1";
1028 $("mochaToolbar").removeClass("invisible");
1030 else {
1031 $("showTopToolbarLink").firstChild.style.opacity = "0";
1032 $("mochaToolbar").addClass("invisible");
1034 MochaUI.Desktop.setDesktopSize();
1037 $("showStatusBarLink").addEvent("click", (e) => {
1038 showStatusBar = !showStatusBar;
1039 LocalPreferences.set("show_status_bar", showStatusBar.toString());
1040 if (showStatusBar) {
1041 $("showStatusBarLink").firstChild.style.opacity = "1";
1042 $("desktopFooterWrapper").removeClass("invisible");
1044 else {
1045 $("showStatusBarLink").firstChild.style.opacity = "0";
1046 $("desktopFooterWrapper").addClass("invisible");
1048 MochaUI.Desktop.setDesktopSize();
1051 const registerMagnetHandler = function() {
1052 if (typeof navigator.registerProtocolHandler !== "function") {
1053 if (window.location.protocol !== "https:")
1054 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1055 else
1056 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1057 return;
1060 const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
1061 const hashParams = new URLSearchParams(hashString);
1062 hashParams.set("download", "");
1064 const templateHashString = hashParams.toString().replace("download=", "download=%s");
1065 const templateUrl = location.origin + location.pathname
1066 + location.search + "#" + templateHashString;
1068 navigator.registerProtocolHandler("magnet", templateUrl,
1069 "qBittorrent WebUI magnet handler");
1071 $("registerMagnetHandlerLink").addEvent("click", (e) => {
1072 registerMagnetHandler();
1075 $("showFiltersSidebarLink").addEvent("click", (e) => {
1076 showFiltersSidebar = !showFiltersSidebar;
1077 LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1078 if (showFiltersSidebar) {
1079 $("showFiltersSidebarLink").firstChild.style.opacity = "1";
1080 $("filtersColumn").removeClass("invisible");
1081 $("filtersColumn_handle").removeClass("invisible");
1083 else {
1084 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
1085 $("filtersColumn").addClass("invisible");
1086 $("filtersColumn_handle").addClass("invisible");
1088 MochaUI.Desktop.setDesktopSize();
1091 $("speedInBrowserTitleBarLink").addEvent("click", (e) => {
1092 speedInTitle = !speedInTitle;
1093 LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1094 if (speedInTitle)
1095 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
1096 else
1097 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
1098 processServerState();
1101 $("showSearchEngineLink").addEvent("click", (e) => {
1102 window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1103 LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1104 updateTabDisplay();
1107 $("showRssReaderLink").addEvent("click", (e) => {
1108 window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1109 LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1110 updateTabDisplay();
1113 $("showLogViewerLink").addEvent("click", (e) => {
1114 window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1115 LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1116 updateTabDisplay();
1119 const updateTabDisplay = function() {
1120 if (window.qBittorrent.Client.isShowRssReader()) {
1121 $("showRssReaderLink").firstChild.style.opacity = "1";
1122 $("mainWindowTabs").removeClass("invisible");
1123 $("rssTabLink").removeClass("invisible");
1124 if (!MochaUI.Panels.instances.RssPanel)
1125 addRssPanel();
1127 else {
1128 $("showRssReaderLink").firstChild.style.opacity = "0";
1129 $("rssTabLink").addClass("invisible");
1130 if ($("rssTabLink").hasClass("selected"))
1131 $("transfersTabLink").click();
1134 if (window.qBittorrent.Client.isShowSearchEngine()) {
1135 $("showSearchEngineLink").firstChild.style.opacity = "1";
1136 $("mainWindowTabs").removeClass("invisible");
1137 $("searchTabLink").removeClass("invisible");
1138 if (!MochaUI.Panels.instances.SearchPanel)
1139 addSearchPanel();
1141 else {
1142 $("showSearchEngineLink").firstChild.style.opacity = "0";
1143 $("searchTabLink").addClass("invisible");
1144 if ($("searchTabLink").hasClass("selected"))
1145 $("transfersTabLink").click();
1148 if (window.qBittorrent.Client.isShowLogViewer()) {
1149 $("showLogViewerLink").firstChild.style.opacity = "1";
1150 $("mainWindowTabs").removeClass("invisible");
1151 $("logTabLink").removeClass("invisible");
1152 if (!MochaUI.Panels.instances.LogPanel)
1153 addLogPanel();
1155 else {
1156 $("showLogViewerLink").firstChild.style.opacity = "0";
1157 $("logTabLink").addClass("invisible");
1158 if ($("logTabLink").hasClass("selected"))
1159 $("transfersTabLink").click();
1162 // display no tabs
1163 if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
1164 $("mainWindowTabs").addClass("invisible");
1167 $("StatisticsLink").addEvent("click", StatisticsLinkFN);
1169 // main window tabs
1171 const showTransfersTab = function() {
1172 const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1173 if (showFiltersSidebar) {
1174 $("filtersColumn").removeClass("invisible");
1175 $("filtersColumn_handle").removeClass("invisible");
1177 $("mainColumn").removeClass("invisible");
1178 $("torrentsFilterToolbar").removeClass("invisible");
1180 customSyncMainDataInterval = null;
1181 syncData(100);
1183 hideSearchTab();
1184 hideRssTab();
1185 hideLogTab();
1187 LocalPreferences.set("selected_window_tab", "transfers");
1190 const hideTransfersTab = function() {
1191 $("filtersColumn").addClass("invisible");
1192 $("filtersColumn_handle").addClass("invisible");
1193 $("mainColumn").addClass("invisible");
1194 $("torrentsFilterToolbar").addClass("invisible");
1195 MochaUI.Desktop.resizePanels();
1198 const showSearchTab = (function() {
1199 let searchTabInitialized = false;
1201 return () => {
1202 // we must wait until the panel is fully loaded before proceeding.
1203 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1204 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1205 if (!isSearchPanelLoaded) {
1206 setTimeout(() => {
1207 showSearchTab();
1208 }, 100);
1209 return;
1212 if (!searchTabInitialized) {
1213 window.qBittorrent.Search.init();
1214 searchTabInitialized = true;
1217 $("searchTabColumn").removeClass("invisible");
1218 customSyncMainDataInterval = 30000;
1219 hideTransfersTab();
1220 hideRssTab();
1221 hideLogTab();
1223 LocalPreferences.set("selected_window_tab", "search");
1225 })();
1227 const hideSearchTab = function() {
1228 $("searchTabColumn").addClass("invisible");
1229 MochaUI.Desktop.resizePanels();
1232 const showRssTab = (function() {
1233 let rssTabInitialized = false;
1235 return () => {
1236 if (!rssTabInitialized) {
1237 window.qBittorrent.Rss.init();
1238 rssTabInitialized = true;
1240 else {
1241 window.qBittorrent.Rss.load();
1244 $("rssTabColumn").removeClass("invisible");
1245 customSyncMainDataInterval = 30000;
1246 hideTransfersTab();
1247 hideSearchTab();
1248 hideLogTab();
1250 LocalPreferences.set("selected_window_tab", "rss");
1252 })();
1254 const hideRssTab = function() {
1255 $("rssTabColumn").addClass("invisible");
1256 window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1257 MochaUI.Desktop.resizePanels();
1260 const showLogTab = (function() {
1261 let logTabInitialized = false;
1263 return () => {
1264 // we must wait until the panel is fully loaded before proceeding.
1265 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1266 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1267 if (!isLogPanelLoaded) {
1268 setTimeout(() => {
1269 showLogTab();
1270 }, 100);
1271 return;
1274 if (!logTabInitialized) {
1275 window.qBittorrent.Log.init();
1276 logTabInitialized = true;
1278 else {
1279 window.qBittorrent.Log.load();
1282 $("logTabColumn").removeClass("invisible");
1283 customSyncMainDataInterval = 30000;
1284 hideTransfersTab();
1285 hideSearchTab();
1286 hideRssTab();
1288 LocalPreferences.set("selected_window_tab", "log");
1290 })();
1292 const hideLogTab = function() {
1293 $("logTabColumn").addClass("invisible");
1294 MochaUI.Desktop.resizePanels();
1295 window.qBittorrent.Log && window.qBittorrent.Log.unload();
1298 const addSearchPanel = function() {
1299 new MochaUI.Panel({
1300 id: "SearchPanel",
1301 title: "Search",
1302 header: false,
1303 padding: {
1304 top: 0,
1305 right: 0,
1306 bottom: 0,
1307 left: 0
1309 loadMethod: "xhr",
1310 contentURL: "views/search.html",
1311 require: {
1312 js: ["scripts/search.js"],
1313 onload: () => {
1314 isSearchPanelLoaded = true;
1317 content: "",
1318 column: "searchTabColumn",
1319 height: null
1323 const addRssPanel = function() {
1324 new MochaUI.Panel({
1325 id: "RssPanel",
1326 title: "Rss",
1327 header: false,
1328 padding: {
1329 top: 0,
1330 right: 0,
1331 bottom: 0,
1332 left: 0
1334 loadMethod: "xhr",
1335 contentURL: "views/rss.html",
1336 content: "",
1337 column: "rssTabColumn",
1338 height: null
1342 const addLogPanel = function() {
1343 new MochaUI.Panel({
1344 id: "LogPanel",
1345 title: "Log",
1346 header: true,
1347 padding: {
1348 top: 0,
1349 right: 0,
1350 bottom: 0,
1351 left: 0
1353 loadMethod: "xhr",
1354 contentURL: "views/log.html",
1355 require: {
1356 css: ["css/vanillaSelectBox.css"],
1357 js: ["scripts/lib/vanillaSelectBox.js"],
1358 onload: () => {
1359 isLogPanelLoaded = true;
1362 tabsURL: "views/logTabs.html",
1363 tabsOnload: function() {
1364 MochaUI.initializeTabs("panelTabs");
1366 $("logMessageLink").addEvent("click", (e) => {
1367 window.qBittorrent.Log.setCurrentTab("main");
1370 $("logPeerLink").addEvent("click", (e) => {
1371 window.qBittorrent.Log.setCurrentTab("peer");
1374 collapsible: false,
1375 content: "",
1376 column: "logTabColumn",
1377 height: null
1381 const handleDownloadParam = function() {
1382 // Extract torrent URL from download param in WebUI URL hash
1383 const downloadHash = "#download=";
1384 if (location.hash.indexOf(downloadHash) !== 0)
1385 return;
1387 const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1388 // Remove the processed hash from the URL
1389 history.replaceState("", document.title, (location.pathname + location.search));
1390 showDownloadPage([url]);
1393 new MochaUI.Panel({
1394 id: "transferList",
1395 title: "Panel",
1396 header: false,
1397 padding: {
1398 top: 0,
1399 right: 0,
1400 bottom: 0,
1401 left: 0
1403 loadMethod: "xhr",
1404 contentURL: "views/transferlist.html",
1405 onContentLoaded: function() {
1406 handleDownloadParam();
1407 updateMainData();
1409 column: "mainColumn",
1410 onResize: saveColumnSizes,
1411 height: null
1413 let prop_h = LocalPreferences.get("properties_height_rel");
1414 if (prop_h !== null)
1415 prop_h = prop_h.toFloat() * Window.getSize().y;
1416 else
1417 prop_h = Window.getSize().y / 2.0;
1418 new MochaUI.Panel({
1419 id: "propertiesPanel",
1420 title: "Panel",
1421 header: true,
1422 padding: {
1423 top: 0,
1424 right: 0,
1425 bottom: 0,
1426 left: 0
1428 contentURL: "views/properties.html",
1429 require: {
1430 css: ["css/Tabs.css", "css/dynamicTable.css"],
1431 js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1433 tabsURL: "views/propertiesToolbar.html",
1434 tabsOnload: function() {
1435 MochaUI.initializeTabs("propertiesTabs");
1437 updatePropertiesPanel = function() {
1438 if (!$("prop_general").hasClass("invisible")) {
1439 if (window.qBittorrent.PropGeneral !== undefined)
1440 window.qBittorrent.PropGeneral.updateData();
1442 else if (!$("prop_trackers").hasClass("invisible")) {
1443 if (window.qBittorrent.PropTrackers !== undefined)
1444 window.qBittorrent.PropTrackers.updateData();
1446 else if (!$("prop_peers").hasClass("invisible")) {
1447 if (window.qBittorrent.PropPeers !== undefined)
1448 window.qBittorrent.PropPeers.updateData();
1450 else if (!$("prop_webseeds").hasClass("invisible")) {
1451 if (window.qBittorrent.PropWebseeds !== undefined)
1452 window.qBittorrent.PropWebseeds.updateData();
1454 else if (!$("prop_files").hasClass("invisible")) {
1455 if (window.qBittorrent.PropFiles !== undefined)
1456 window.qBittorrent.PropFiles.updateData();
1460 $("PropGeneralLink").addEvent("click", function(e) {
1461 $$(".propertiesTabContent").addClass("invisible");
1462 $("prop_general").removeClass("invisible");
1463 hideFilesFilter();
1464 updatePropertiesPanel();
1465 LocalPreferences.set("selected_tab", this.id);
1468 $("PropTrackersLink").addEvent("click", function(e) {
1469 $$(".propertiesTabContent").addClass("invisible");
1470 $("prop_trackers").removeClass("invisible");
1471 hideFilesFilter();
1472 updatePropertiesPanel();
1473 LocalPreferences.set("selected_tab", this.id);
1476 $("PropPeersLink").addEvent("click", function(e) {
1477 $$(".propertiesTabContent").addClass("invisible");
1478 $("prop_peers").removeClass("invisible");
1479 hideFilesFilter();
1480 updatePropertiesPanel();
1481 LocalPreferences.set("selected_tab", this.id);
1484 $("PropWebSeedsLink").addEvent("click", function(e) {
1485 $$(".propertiesTabContent").addClass("invisible");
1486 $("prop_webseeds").removeClass("invisible");
1487 hideFilesFilter();
1488 updatePropertiesPanel();
1489 LocalPreferences.set("selected_tab", this.id);
1492 $("PropFilesLink").addEvent("click", function(e) {
1493 $$(".propertiesTabContent").addClass("invisible");
1494 $("prop_files").removeClass("invisible");
1495 showFilesFilter();
1496 updatePropertiesPanel();
1497 LocalPreferences.set("selected_tab", this.id);
1500 $("propertiesPanel_collapseToggle").addEvent("click", (e) => {
1501 updatePropertiesPanel();
1504 column: "mainColumn",
1505 height: prop_h
1508 const showFilesFilter = function() {
1509 $("torrentFilesFilterToolbar").removeClass("invisible");
1512 const hideFilesFilter = function() {
1513 $("torrentFilesFilterToolbar").addClass("invisible");
1516 // listen for changes to torrentsFilterInput
1517 let torrentsFilterInputTimer = -1;
1518 $("torrentsFilterInput").addEvent("input", () => {
1519 clearTimeout(torrentsFilterInputTimer);
1520 torrentsFilterInputTimer = setTimeout(() => {
1521 torrentsFilterInputTimer = -1;
1522 torrentsTable.updateTable();
1523 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1525 $("torrentsFilterRegexBox").addEvent("change", () => {
1526 torrentsTable.updateTable();
1529 $("transfersTabLink").addEvent("click", showTransfersTab);
1530 $("searchTabLink").addEvent("click", showSearchTab);
1531 $("rssTabLink").addEvent("click", showRssTab);
1532 $("logTabLink").addEvent("click", showLogTab);
1533 updateTabDisplay();
1535 const registerDragAndDrop = () => {
1536 $("desktop").addEventListener("dragover", (ev) => {
1537 if (ev.preventDefault)
1538 ev.preventDefault();
1541 $("desktop").addEventListener("dragenter", (ev) => {
1542 if (ev.preventDefault)
1543 ev.preventDefault();
1546 $("desktop").addEventListener("drop", (ev) => {
1547 if (ev.preventDefault)
1548 ev.preventDefault();
1550 const droppedFiles = ev.dataTransfer.files;
1552 if (droppedFiles.length > 0) {
1553 // dropped files or folders
1555 // can't handle folder due to cannot put the filelist (from dropped folder)
1556 // to <input> `files` field
1557 for (const item of ev.dataTransfer.items) {
1558 if (item.webkitGetAsEntry().isDirectory)
1559 return;
1562 const id = "uploadPage";
1563 new MochaUI.Window({
1564 id: id,
1565 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1566 loadMethod: "iframe",
1567 contentURL: new URI("upload.html").toString(),
1568 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1569 scrollbars: true,
1570 maximizable: false,
1571 paddingVertical: 0,
1572 paddingHorizontal: 0,
1573 width: loadWindowWidth(id, 500),
1574 height: loadWindowHeight(id, 460),
1575 onResize: () => {
1576 saveWindowSize(id);
1578 onContentLoaded: () => {
1579 const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
1580 fileInput.files = droppedFiles;
1585 const droppedText = ev.dataTransfer.getData("text");
1586 if (droppedText.length > 0) {
1587 // dropped text
1589 const urls = droppedText.split("\n")
1590 .map((str) => str.trim())
1591 .filter((str) => {
1592 const lowercaseStr = str.toLowerCase();
1593 return lowercaseStr.startsWith("http:")
1594 || lowercaseStr.startsWith("https:")
1595 || lowercaseStr.startsWith("magnet:")
1596 || ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
1597 || ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
1600 if (urls.length <= 0)
1601 return;
1603 const id = "downloadPage";
1604 const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
1605 new MochaUI.Window({
1606 id: id,
1607 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1608 loadMethod: "iframe",
1609 contentURL: contentURI.toString(),
1610 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1611 scrollbars: true,
1612 maximizable: false,
1613 closable: true,
1614 paddingVertical: 0,
1615 paddingHorizontal: 0,
1616 width: loadWindowWidth(id, 500),
1617 height: loadWindowHeight(id, 600),
1618 onResize: () => {
1619 saveWindowSize(id);
1625 registerDragAndDrop();
1627 new Keyboard({
1628 defaultEventType: "keydown",
1629 events: {
1630 "ctrl+a": function(event) {
1631 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1632 return;
1633 if (event.target.isContentEditable)
1634 return;
1635 torrentsTable.selectAll();
1636 event.preventDefault();
1638 "delete": function(event) {
1639 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1640 return;
1641 if (event.target.isContentEditable)
1642 return;
1643 deleteFN();
1644 event.preventDefault();
1646 "shift+delete": (event) => {
1647 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1648 return;
1649 if (event.target.isContentEditable)
1650 return;
1651 deleteFN(true);
1652 event.preventDefault();
1655 }).activate();
1658 window.addEventListener("load", () => {
1659 // fetch various data and store it in memory
1660 window.qBittorrent.Cache.buildInfo.init();
1661 window.qBittorrent.Cache.preferences.init();
1662 window.qBittorrent.Cache.qbtVersion.init();
1664 // switch to previously used tab
1665 const previouslyUsedTab = LocalPreferences.get("selected_window_tab", "transfers");
1666 switch (previouslyUsedTab) {
1667 case "search":
1668 if (window.qBittorrent.Client.isShowSearchEngine())
1669 $("searchTabLink").click();
1670 break;
1671 case "rss":
1672 if (window.qBittorrent.Client.isShowRssReader())
1673 $("rssTabLink").click();
1674 break;
1675 case "log":
1676 if (window.qBittorrent.Client.isShowLogViewer())
1677 $("logTabLink").click();
1678 break;
1679 case "transfers":
1680 $("transfersTabLink").click();
1681 break;
1682 default:
1683 console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1684 $("transfersTabLink").click();
1685 break;