mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-18 06:28:03 -06:00
It is now possible to expand & collapse it by clicking directly on tabs, just like in GUI. In addition, collapse state is saved and applied on page load. Fixed one minor bug and now files search input is properly hidden even when panel is collapsed. PR #21209.
1721 lines
67 KiB
JavaScript
1721 lines
67 KiB
JavaScript
/*
|
|
* MIT License
|
|
* Copyright (C) 2024 Mike Tzou (Chocobo1)
|
|
* Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>,
|
|
* Christophe Dumez <chris@qbittorrent.org>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
window.qBittorrent ??= {};
|
|
window.qBittorrent.Client ??= (() => {
|
|
const exports = () => {
|
|
return {
|
|
closeWindows: closeWindows,
|
|
genHash: genHash,
|
|
getSyncMainDataInterval: getSyncMainDataInterval,
|
|
isStopped: isStopped,
|
|
stop: stop,
|
|
mainTitle: mainTitle,
|
|
showSearchEngine: showSearchEngine,
|
|
showRssReader: showRssReader,
|
|
showLogViewer: showLogViewer,
|
|
isShowSearchEngine: isShowSearchEngine,
|
|
isShowRssReader: isShowRssReader,
|
|
isShowLogViewer: isShowLogViewer
|
|
};
|
|
};
|
|
|
|
const closeWindows = function() {
|
|
MochaUI.closeAll();
|
|
};
|
|
|
|
const genHash = function(string) {
|
|
// origins:
|
|
// https://stackoverflow.com/a/8831937
|
|
// https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
|
|
let hash = 0;
|
|
for (let i = 0; i < string.length; ++i)
|
|
hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
|
|
return hash;
|
|
};
|
|
|
|
const getSyncMainDataInterval = function() {
|
|
return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
|
|
};
|
|
|
|
let stopped = false;
|
|
const isStopped = () => {
|
|
return stopped;
|
|
};
|
|
|
|
const stop = () => {
|
|
stopped = true;
|
|
};
|
|
|
|
const mainTitle = () => {
|
|
const emDash = "\u2014";
|
|
const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
|
|
const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
|
|
const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
|
|
+ ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
|
|
return title;
|
|
};
|
|
|
|
let showingSearchEngine = false;
|
|
let showingRssReader = false;
|
|
let showingLogViewer = false;
|
|
|
|
const showSearchEngine = function(bool) {
|
|
showingSearchEngine = bool;
|
|
};
|
|
const showRssReader = function(bool) {
|
|
showingRssReader = bool;
|
|
};
|
|
const showLogViewer = function(bool) {
|
|
showingLogViewer = bool;
|
|
};
|
|
const isShowSearchEngine = function() {
|
|
return showingSearchEngine;
|
|
};
|
|
const isShowRssReader = function() {
|
|
return showingRssReader;
|
|
};
|
|
const isShowLogViewer = function() {
|
|
return showingLogViewer;
|
|
};
|
|
|
|
return exports();
|
|
})();
|
|
Object.freeze(window.qBittorrent.Client);
|
|
|
|
// TODO: move global functions/variables into some namespace/scope
|
|
|
|
this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
|
|
|
|
let updatePropertiesPanel = function() {};
|
|
|
|
this.updateMainData = function() {};
|
|
let alternativeSpeedLimits = false;
|
|
let queueing_enabled = true;
|
|
let serverSyncMainDataInterval = 1500;
|
|
let customSyncMainDataInterval = null;
|
|
let useSubcategories = true;
|
|
const useAutoHideZeroStatusFilters = LocalPreferences.get("hide_zero_status_filters", "false") === "true";
|
|
|
|
/* Categories filter */
|
|
const CATEGORIES_ALL = 1;
|
|
const CATEGORIES_UNCATEGORIZED = 2;
|
|
|
|
const category_list = new Map();
|
|
|
|
let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
|
|
let setCategoryFilter = function() {};
|
|
|
|
/* Tags filter */
|
|
const TAGS_ALL = 1;
|
|
const TAGS_UNTAGGED = 2;
|
|
|
|
const tagList = new Map();
|
|
|
|
let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
|
|
let setTagFilter = function() {};
|
|
|
|
/* Trackers filter */
|
|
const TRACKERS_ALL = 1;
|
|
const TRACKERS_TRACKERLESS = 2;
|
|
|
|
/** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
|
|
const trackerList = new Map();
|
|
|
|
let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
|
|
let setTrackerFilter = function() {};
|
|
|
|
/* All filters */
|
|
let selected_filter = LocalPreferences.get("selected_filter", "all");
|
|
let setFilter = function() {};
|
|
let toggleFilterDisplay = function() {};
|
|
|
|
window.addEventListener("DOMContentLoaded", () => {
|
|
let isSearchPanelLoaded = false;
|
|
let isLogPanelLoaded = false;
|
|
|
|
const saveColumnSizes = function() {
|
|
const filters_width = $("Filters").getSize().x;
|
|
LocalPreferences.set("filters_width", filters_width);
|
|
const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
|
|
LocalPreferences.set("properties_height_rel", properties_height_rel);
|
|
};
|
|
|
|
window.addEventListener("resize", window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
|
// only save sizes if the columns are visible
|
|
if (!$("mainColumn").hasClass("invisible"))
|
|
saveColumnSizes();
|
|
}));
|
|
|
|
/* MochaUI.Desktop = new MochaUI.Desktop();
|
|
MochaUI.Desktop.desktop.style.background = "#fff";
|
|
MochaUI.Desktop.desktop.style.visibility = "visible"; */
|
|
MochaUI.Desktop.initialize();
|
|
|
|
const buildTransfersTab = function() {
|
|
const filt_w = Number(LocalPreferences.get("filters_width", 120));
|
|
new MochaUI.Column({
|
|
id: "filtersColumn",
|
|
placement: "left",
|
|
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
|
saveColumnSizes();
|
|
}),
|
|
width: filt_w,
|
|
resizeLimit: [1, 300]
|
|
});
|
|
new MochaUI.Column({
|
|
id: "mainColumn",
|
|
placement: "main"
|
|
});
|
|
};
|
|
|
|
const buildSearchTab = function() {
|
|
new MochaUI.Column({
|
|
id: "searchTabColumn",
|
|
placement: "main",
|
|
width: null
|
|
});
|
|
|
|
// start off hidden
|
|
$("searchTabColumn").addClass("invisible");
|
|
};
|
|
|
|
const buildRssTab = function() {
|
|
new MochaUI.Column({
|
|
id: "rssTabColumn",
|
|
placement: "main",
|
|
width: null
|
|
});
|
|
|
|
// start off hidden
|
|
$("rssTabColumn").addClass("invisible");
|
|
};
|
|
|
|
const buildLogTab = function() {
|
|
new MochaUI.Column({
|
|
id: "logTabColumn",
|
|
placement: "main",
|
|
width: null
|
|
});
|
|
|
|
// start off hidden
|
|
$("logTabColumn").addClass("invisible");
|
|
};
|
|
|
|
buildTransfersTab();
|
|
buildSearchTab();
|
|
buildRssTab();
|
|
buildLogTab();
|
|
MochaUI.initializeTabs("mainWindowTabsList");
|
|
|
|
setCategoryFilter = function(hash) {
|
|
selected_category = hash;
|
|
LocalPreferences.set("selected_category", selected_category);
|
|
highlightSelectedCategory();
|
|
if (typeof torrentsTable.tableBody !== "undefined")
|
|
updateMainData();
|
|
};
|
|
|
|
setTagFilter = function(hash) {
|
|
selectedTag = hash;
|
|
LocalPreferences.set("selected_tag", selectedTag);
|
|
highlightSelectedTag();
|
|
if (torrentsTable.tableBody !== undefined)
|
|
updateMainData();
|
|
};
|
|
|
|
setTrackerFilter = function(hash) {
|
|
selectedTracker = hash.toString();
|
|
LocalPreferences.set("selected_tracker", selectedTracker);
|
|
highlightSelectedTracker();
|
|
if (torrentsTable.tableBody !== undefined)
|
|
updateMainData();
|
|
};
|
|
|
|
setFilter = function(f) {
|
|
// Visually Select the right filter
|
|
$("all_filter").removeClass("selectedFilter");
|
|
$("downloading_filter").removeClass("selectedFilter");
|
|
$("seeding_filter").removeClass("selectedFilter");
|
|
$("completed_filter").removeClass("selectedFilter");
|
|
$("stopped_filter").removeClass("selectedFilter");
|
|
$("running_filter").removeClass("selectedFilter");
|
|
$("active_filter").removeClass("selectedFilter");
|
|
$("inactive_filter").removeClass("selectedFilter");
|
|
$("stalled_filter").removeClass("selectedFilter");
|
|
$("stalled_uploading_filter").removeClass("selectedFilter");
|
|
$("stalled_downloading_filter").removeClass("selectedFilter");
|
|
$("checking_filter").removeClass("selectedFilter");
|
|
$("moving_filter").removeClass("selectedFilter");
|
|
$("errored_filter").removeClass("selectedFilter");
|
|
$(f + "_filter").addClass("selectedFilter");
|
|
selected_filter = f;
|
|
LocalPreferences.set("selected_filter", f);
|
|
// Reload torrents
|
|
if (typeof torrentsTable.tableBody !== "undefined")
|
|
updateMainData();
|
|
};
|
|
|
|
toggleFilterDisplay = function(filter) {
|
|
const element = filter + "FilterList";
|
|
LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
|
|
$(element).toggleClass("invisible");
|
|
const parent = $(element).getParent(".filterWrapper");
|
|
const toggleIcon = $(parent).getChildren(".filterTitle img");
|
|
if (toggleIcon)
|
|
toggleIcon[0].toggleClass("rotate");
|
|
};
|
|
|
|
new MochaUI.Panel({
|
|
id: "Filters",
|
|
title: "Panel",
|
|
header: false,
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
loadMethod: "xhr",
|
|
contentURL: "views/filters.html",
|
|
onContentLoaded: function() {
|
|
setFilter(selected_filter);
|
|
},
|
|
column: "filtersColumn",
|
|
height: 300
|
|
});
|
|
initializeWindows();
|
|
|
|
// Show Top Toolbar is enabled by default
|
|
let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
|
|
if (!showTopToolbar) {
|
|
$("showTopToolbarLink").firstChild.style.opacity = "0";
|
|
$("mochaToolbar").addClass("invisible");
|
|
}
|
|
|
|
// Show Status Bar is enabled by default
|
|
let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
|
|
if (!showStatusBar) {
|
|
$("showStatusBarLink").firstChild.style.opacity = "0";
|
|
$("desktopFooterWrapper").addClass("invisible");
|
|
}
|
|
|
|
// Show Filters Sidebar is enabled by default
|
|
let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
|
|
if (!showFiltersSidebar) {
|
|
$("showFiltersSidebarLink").firstChild.style.opacity = "0";
|
|
$("filtersColumn").addClass("invisible");
|
|
$("filtersColumn_handle").addClass("invisible");
|
|
}
|
|
|
|
let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
|
|
if (!speedInTitle)
|
|
$("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
|
|
|
|
// After showing/hiding the toolbar + status bar
|
|
window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
|
|
window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
|
|
window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
|
|
|
|
// After Show Top Toolbar
|
|
MochaUI.Desktop.setDesktopSize();
|
|
|
|
let syncMainDataLastResponseId = 0;
|
|
const serverState = {};
|
|
|
|
const removeTorrentFromCategoryList = function(hash) {
|
|
if (!hash)
|
|
return false;
|
|
|
|
let removed = false;
|
|
category_list.forEach((category) => {
|
|
const deleteResult = category.torrents.delete(hash);
|
|
removed ||= deleteResult;
|
|
});
|
|
|
|
return removed;
|
|
};
|
|
|
|
const addTorrentToCategoryList = function(torrent) {
|
|
const category = torrent["category"];
|
|
if (typeof category === "undefined")
|
|
return false;
|
|
|
|
const hash = torrent["hash"];
|
|
if (category.length === 0) { // Empty category
|
|
removeTorrentFromCategoryList(hash);
|
|
return true;
|
|
}
|
|
|
|
const categoryHash = window.qBittorrent.Client.genHash(category);
|
|
if (!category_list.has(categoryHash)) { // This should not happen
|
|
category_list.set(categoryHash, {
|
|
name: category,
|
|
torrents: new Set()
|
|
});
|
|
}
|
|
|
|
const torrents = category_list.get(categoryHash).torrents;
|
|
if (!torrents.has(hash)) {
|
|
removeTorrentFromCategoryList(hash);
|
|
torrents.add(hash);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const removeTorrentFromTagList = function(hash) {
|
|
if (!hash)
|
|
return false;
|
|
|
|
let removed = false;
|
|
tagList.forEach((tag) => {
|
|
const deleteResult = tag.torrents.delete(hash);
|
|
removed ||= deleteResult;
|
|
});
|
|
|
|
return removed;
|
|
};
|
|
|
|
const addTorrentToTagList = function(torrent) {
|
|
if (torrent["tags"] === undefined) // Tags haven't changed
|
|
return false;
|
|
|
|
const hash = torrent["hash"];
|
|
removeTorrentFromTagList(hash);
|
|
|
|
if (torrent["tags"].length === 0) // No tags
|
|
return true;
|
|
|
|
const tags = torrent["tags"].split(",");
|
|
let added = false;
|
|
for (let i = 0; i < tags.length; ++i) {
|
|
const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
|
|
if (!tagList.has(tagHash)) { // This should not happen
|
|
tagList.set(tagHash, {
|
|
name: tags,
|
|
torrents: new Set()
|
|
});
|
|
}
|
|
|
|
const torrents = tagList.get(tagHash).torrents;
|
|
if (!torrents.has(hash)) {
|
|
torrents.add(hash);
|
|
added = true;
|
|
}
|
|
}
|
|
return added;
|
|
};
|
|
|
|
const updateFilter = function(filter, filterTitle) {
|
|
const filterEl = document.getElementById(`${filter}_filter`);
|
|
const filterTorrentCount = torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
|
|
if (useAutoHideZeroStatusFilters) {
|
|
const hideFilter = (filterTorrentCount === 0) && (filter !== "all");
|
|
if (filterEl.classList.toggle("invisible", hideFilter))
|
|
return;
|
|
}
|
|
filterEl.firstElementChild.lastChild.nodeValue = filterTitle.replace("%1", filterTorrentCount);
|
|
};
|
|
|
|
const updateFiltersList = function() {
|
|
updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
|
|
};
|
|
|
|
const updateCategoryList = function() {
|
|
const categoryList = $("categoryFilterList");
|
|
if (!categoryList)
|
|
return;
|
|
categoryList.getChildren().each(c => c.destroy());
|
|
|
|
const create_link = function(hash, text, count) {
|
|
let display_name = text;
|
|
let margin_left = 0;
|
|
if (useSubcategories) {
|
|
const category_path = text.split("/");
|
|
display_name = category_path[category_path.length - 1];
|
|
margin_left = (category_path.length - 1) * 20;
|
|
}
|
|
|
|
const span = document.createElement("span");
|
|
span.classList.add("link");
|
|
span.href = "#";
|
|
span.style.marginLeft = `${margin_left}px`;
|
|
span.textContent = `${display_name} (${count})`;
|
|
span.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
setCategoryFilter(hash);
|
|
});
|
|
|
|
const img = document.createElement("img");
|
|
img.src = "images/view-categories.svg";
|
|
span.prepend(img);
|
|
|
|
const listItem = document.createElement("li");
|
|
listItem.id = hash;
|
|
listItem.appendChild(span);
|
|
|
|
window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(listItem);
|
|
return listItem;
|
|
};
|
|
|
|
const all = torrentsTable.getRowIds().length;
|
|
let uncategorized = 0;
|
|
for (const key in torrentsTable.rows) {
|
|
if (!Object.hasOwn(torrentsTable.rows, key))
|
|
continue;
|
|
|
|
const row = torrentsTable.rows[key];
|
|
if (row["full_data"].category.length === 0)
|
|
uncategorized += 1;
|
|
}
|
|
categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
|
|
categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
|
|
|
|
const sortedCategories = [];
|
|
category_list.forEach((category, hash) => sortedCategories.push({
|
|
categoryName: category.name,
|
|
categoryHash: hash,
|
|
categoryCount: category.torrents.size
|
|
}));
|
|
sortedCategories.sort((left, right) => {
|
|
const leftSegments = left.categoryName.split("/");
|
|
const rightSegments = right.categoryName.split("/");
|
|
|
|
for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
|
|
const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
|
|
leftSegments[i], rightSegments[i]);
|
|
if (compareResult !== 0)
|
|
return compareResult;
|
|
}
|
|
|
|
return leftSegments.length - rightSegments.length;
|
|
});
|
|
|
|
for (let i = 0; i < sortedCategories.length; ++i) {
|
|
const { categoryName, categoryHash } = sortedCategories[i];
|
|
let { categoryCount } = sortedCategories[i];
|
|
|
|
if (useSubcategories) {
|
|
for (let j = (i + 1);
|
|
((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
|
|
categoryCount += sortedCategories[j].categoryCount;
|
|
}
|
|
|
|
categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
|
|
}
|
|
|
|
highlightSelectedCategory();
|
|
};
|
|
|
|
const highlightSelectedCategory = function() {
|
|
const categoryList = $("categoryFilterList");
|
|
if (!categoryList)
|
|
return;
|
|
const children = categoryList.childNodes;
|
|
for (let i = 0; i < children.length; ++i) {
|
|
if (Number(children[i].id) === selected_category)
|
|
children[i].className = "selectedFilter";
|
|
else
|
|
children[i].className = "";
|
|
}
|
|
};
|
|
|
|
const updateTagList = function() {
|
|
const tagFilterList = $("tagFilterList");
|
|
if (tagFilterList === null)
|
|
return;
|
|
|
|
tagFilterList.getChildren().each(c => c.destroy());
|
|
|
|
const createLink = function(hash, text, count) {
|
|
const span = document.createElement("span");
|
|
span.classList.add("link");
|
|
span.href = "#";
|
|
span.textContent = `${text} (${count})`;
|
|
span.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
setTagFilter(hash);
|
|
});
|
|
|
|
const img = document.createElement("img");
|
|
img.src = "images/tags.svg";
|
|
span.prepend(img);
|
|
|
|
const listItem = document.createElement("li");
|
|
listItem.id = hash;
|
|
listItem.appendChild(span);
|
|
|
|
window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(listItem);
|
|
return listItem;
|
|
};
|
|
|
|
const torrentsCount = torrentsTable.getRowIds().length;
|
|
let untagged = 0;
|
|
for (const key in torrentsTable.rows) {
|
|
if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].tags.length === 0))
|
|
untagged += 1;
|
|
}
|
|
tagFilterList.appendChild(createLink(TAGS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount));
|
|
tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
|
|
|
|
const sortedTags = [];
|
|
tagList.forEach((tag, hash) => sortedTags.push({
|
|
tagName: tag.name,
|
|
tagHash: hash,
|
|
tagSize: tag.torrents.size
|
|
}));
|
|
sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
|
|
|
|
for (const { tagName, tagHash, tagSize } of sortedTags)
|
|
tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
|
|
|
|
highlightSelectedTag();
|
|
};
|
|
|
|
const highlightSelectedTag = function() {
|
|
const tagFilterList = $("tagFilterList");
|
|
if (!tagFilterList)
|
|
return;
|
|
|
|
const children = tagFilterList.childNodes;
|
|
for (let i = 0; i < children.length; ++i)
|
|
children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
|
|
};
|
|
|
|
// getHost emulate the GUI version `QString getHost(const QString &url)`
|
|
const getHost = function(url) {
|
|
// We want the hostname.
|
|
// If failed to parse the domain, original input should be returned
|
|
|
|
if (!/^(?:https?|udp):/i.test(url))
|
|
return url;
|
|
|
|
try {
|
|
// hack: URL can not get hostname from udp protocol
|
|
const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
|
|
// host: "example.com:8443"
|
|
// hostname: "example.com"
|
|
const host = parsedUrl.hostname;
|
|
if (!host)
|
|
return url;
|
|
|
|
return host;
|
|
}
|
|
catch (error) {
|
|
return url;
|
|
}
|
|
};
|
|
|
|
const updateTrackerList = function() {
|
|
const trackerFilterList = $("trackerFilterList");
|
|
if (trackerFilterList === null)
|
|
return;
|
|
|
|
trackerFilterList.getChildren().each(c => c.destroy());
|
|
|
|
const createLink = function(hash, text, count) {
|
|
const span = document.createElement("span");
|
|
span.classList.add("link");
|
|
span.href = "#";
|
|
span.textContent = text.replace("%1", count);
|
|
span.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
setTrackerFilter(hash);
|
|
});
|
|
|
|
const img = document.createElement("img");
|
|
img.src = "images/trackers.svg";
|
|
span.prepend(img);
|
|
|
|
const listItem = document.createElement("li");
|
|
listItem.id = hash;
|
|
listItem.appendChild(span);
|
|
|
|
window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(listItem);
|
|
return listItem;
|
|
};
|
|
|
|
const torrentsCount = torrentsTable.getRowIds().length;
|
|
trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
|
|
let trackerlessTorrentsCount = 0;
|
|
for (const key in torrentsTable.rows) {
|
|
if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
|
|
trackerlessTorrentsCount += 1;
|
|
}
|
|
trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
|
|
|
|
// Sort trackers by hostname
|
|
const sortedList = [];
|
|
trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
|
|
const uniqueTorrents = new Set();
|
|
for (const torrents of trackerTorrentMap.values()) {
|
|
for (const torrent of torrents)
|
|
uniqueTorrents.add(torrent);
|
|
}
|
|
|
|
sortedList.push({
|
|
trackerHost: host,
|
|
trackerHash: hash,
|
|
trackerCount: uniqueTorrents.size,
|
|
});
|
|
});
|
|
sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
|
|
for (const { trackerHost, trackerHash, trackerCount } of sortedList)
|
|
trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
|
|
|
|
highlightSelectedTracker();
|
|
};
|
|
|
|
const highlightSelectedTracker = function() {
|
|
const trackerFilterList = $("trackerFilterList");
|
|
if (!trackerFilterList)
|
|
return;
|
|
|
|
const children = trackerFilterList.childNodes;
|
|
for (const child of children)
|
|
child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
|
|
};
|
|
|
|
const setupCopyEventHandler = (function() {
|
|
let clipboardEvent;
|
|
|
|
return () => {
|
|
if (clipboardEvent)
|
|
clipboardEvent.destroy();
|
|
|
|
clipboardEvent = new ClipboardJS(".copyToClipboard", {
|
|
text: function(trigger) {
|
|
switch (trigger.id) {
|
|
case "copyName":
|
|
return copyNameFN();
|
|
case "copyInfohash1":
|
|
return copyInfohashFN(1);
|
|
case "copyInfohash2":
|
|
return copyInfohashFN(2);
|
|
case "copyMagnetLink":
|
|
return copyMagnetLinkFN();
|
|
case "copyID":
|
|
return copyIdFN();
|
|
case "copyComment":
|
|
return copyCommentFN();
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
});
|
|
};
|
|
})();
|
|
|
|
let syncMainDataTimeoutID = -1;
|
|
let syncRequestInProgress = false;
|
|
const syncMainData = function() {
|
|
const url = new URI("api/v2/sync/maindata");
|
|
url.setData("rid", syncMainDataLastResponseId);
|
|
const request = new Request.JSON({
|
|
url: url,
|
|
noCache: true,
|
|
method: "get",
|
|
onFailure: function() {
|
|
const errorDiv = $("error_div");
|
|
if (errorDiv)
|
|
errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
|
|
syncRequestInProgress = false;
|
|
syncData(2000);
|
|
},
|
|
onSuccess: function(response) {
|
|
$("error_div").textContent = "";
|
|
if (response) {
|
|
clearTimeout(torrentsFilterInputTimer);
|
|
torrentsFilterInputTimer = -1;
|
|
|
|
let torrentsTableSelectedRows;
|
|
let update_categories = false;
|
|
let updateTags = false;
|
|
let updateTrackers = false;
|
|
const full_update = (response["full_update"] === true);
|
|
if (full_update) {
|
|
torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
|
|
update_categories = true;
|
|
updateTags = true;
|
|
updateTrackers = true;
|
|
torrentsTable.clear();
|
|
category_list.clear();
|
|
tagList.clear();
|
|
trackerList.clear();
|
|
}
|
|
if (response["rid"])
|
|
syncMainDataLastResponseId = response["rid"];
|
|
if (response["categories"]) {
|
|
for (const key in response["categories"]) {
|
|
if (!Object.hasOwn(response["categories"], key))
|
|
continue;
|
|
|
|
const responseCategory = response["categories"][key];
|
|
const categoryHash = window.qBittorrent.Client.genHash(key);
|
|
const category = category_list.get(categoryHash);
|
|
if (category !== undefined) {
|
|
// only the save path can change for existing categories
|
|
category.savePath = responseCategory.savePath;
|
|
}
|
|
else {
|
|
category_list.set(categoryHash, {
|
|
name: responseCategory.name,
|
|
savePath: responseCategory.savePath,
|
|
torrents: new Set()
|
|
});
|
|
}
|
|
}
|
|
update_categories = true;
|
|
}
|
|
if (response["categories_removed"]) {
|
|
response["categories_removed"].each((category) => {
|
|
const categoryHash = window.qBittorrent.Client.genHash(category);
|
|
category_list.delete(categoryHash);
|
|
});
|
|
update_categories = true;
|
|
}
|
|
if (response["tags"]) {
|
|
for (const tag of response["tags"]) {
|
|
const tagHash = window.qBittorrent.Client.genHash(tag);
|
|
if (!tagList.has(tagHash)) {
|
|
tagList.set(tagHash, {
|
|
name: tag,
|
|
torrents: new Set()
|
|
});
|
|
}
|
|
}
|
|
updateTags = true;
|
|
}
|
|
if (response["tags_removed"]) {
|
|
for (let i = 0; i < response["tags_removed"].length; ++i) {
|
|
const tagHash = window.qBittorrent.Client.genHash(response["tags_removed"][i]);
|
|
tagList.delete(tagHash);
|
|
}
|
|
updateTags = true;
|
|
}
|
|
if (response["trackers"]) {
|
|
for (const [tracker, torrents] of Object.entries(response["trackers"])) {
|
|
const host = getHost(tracker);
|
|
const hash = window.qBittorrent.Client.genHash(host);
|
|
|
|
let trackerListItem = trackerList.get(hash);
|
|
if (trackerListItem === undefined) {
|
|
trackerListItem = { host: host, trackerTorrentMap: new Map() };
|
|
trackerList.set(hash, trackerListItem);
|
|
}
|
|
|
|
trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
|
|
}
|
|
updateTrackers = true;
|
|
}
|
|
if (response["trackers_removed"]) {
|
|
for (let i = 0; i < response["trackers_removed"].length; ++i) {
|
|
const tracker = response["trackers_removed"][i];
|
|
const hash = window.qBittorrent.Client.genHash(getHost(tracker));
|
|
const trackerListEntry = trackerList.get(hash);
|
|
if (trackerListEntry)
|
|
trackerListEntry.trackerTorrentMap.delete(tracker);
|
|
}
|
|
updateTrackers = true;
|
|
}
|
|
if (response["torrents"]) {
|
|
let updateTorrentList = false;
|
|
for (const key in response["torrents"]) {
|
|
if (!Object.hasOwn(response["torrents"], key))
|
|
continue;
|
|
|
|
response["torrents"][key]["hash"] = key;
|
|
response["torrents"][key]["rowId"] = key;
|
|
if (response["torrents"][key]["state"])
|
|
response["torrents"][key]["status"] = response["torrents"][key]["state"];
|
|
torrentsTable.updateRowData(response["torrents"][key]);
|
|
if (addTorrentToCategoryList(response["torrents"][key]))
|
|
update_categories = true;
|
|
if (addTorrentToTagList(response["torrents"][key]))
|
|
updateTags = true;
|
|
if (response["torrents"][key]["name"])
|
|
updateTorrentList = true;
|
|
}
|
|
|
|
if (updateTorrentList)
|
|
setupCopyEventHandler();
|
|
}
|
|
if (response["torrents_removed"]) {
|
|
response["torrents_removed"].each((hash) => {
|
|
torrentsTable.removeRow(hash);
|
|
removeTorrentFromCategoryList(hash);
|
|
update_categories = true; // Always to update All category
|
|
removeTorrentFromTagList(hash);
|
|
updateTags = true; // Always to update All tag
|
|
});
|
|
}
|
|
torrentsTable.updateTable(full_update);
|
|
if (response["server_state"]) {
|
|
const tmp = response["server_state"];
|
|
for (const k in tmp) {
|
|
if (!Object.hasOwn(tmp, k))
|
|
continue;
|
|
serverState[k] = tmp[k];
|
|
}
|
|
processServerState();
|
|
}
|
|
updateFiltersList();
|
|
if (update_categories) {
|
|
updateCategoryList();
|
|
window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
|
|
}
|
|
if (updateTags) {
|
|
updateTagList();
|
|
window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
|
|
}
|
|
if (updateTrackers)
|
|
updateTrackerList();
|
|
|
|
if (full_update)
|
|
// re-select previously selected rows
|
|
torrentsTable.reselectRows(torrentsTableSelectedRows);
|
|
}
|
|
syncRequestInProgress = false;
|
|
syncData(window.qBittorrent.Client.getSyncMainDataInterval());
|
|
}
|
|
});
|
|
syncRequestInProgress = true;
|
|
request.send();
|
|
};
|
|
|
|
updateMainData = function() {
|
|
torrentsTable.updateTable();
|
|
syncData(100);
|
|
};
|
|
|
|
const syncData = function(delay) {
|
|
if (syncRequestInProgress)
|
|
return;
|
|
|
|
clearTimeout(syncMainDataTimeoutID);
|
|
syncMainDataTimeoutID = -1;
|
|
|
|
if (window.qBittorrent.Client.isStopped())
|
|
return;
|
|
|
|
syncMainDataTimeoutID = syncMainData.delay(delay);
|
|
};
|
|
|
|
const processServerState = function() {
|
|
let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
|
|
if (serverState.dl_rate_limit > 0)
|
|
transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
|
|
transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
|
|
$("DlInfos").textContent = transfer_info;
|
|
transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
|
|
if (serverState.up_rate_limit > 0)
|
|
transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
|
|
transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
|
|
$("UpInfos").textContent = transfer_info;
|
|
|
|
document.title = (speedInTitle
|
|
? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
|
|
.replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
|
|
.replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
|
|
: "")
|
|
+ window.qBittorrent.Client.mainTitle();
|
|
|
|
$("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk));
|
|
$("DHTNodes").textContent = "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes);
|
|
|
|
// Statistics dialog
|
|
if (document.getElementById("statisticsContent")) {
|
|
$("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false);
|
|
$("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false);
|
|
$("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false);
|
|
$("GlobalRatio").textContent = serverState.global_ratio;
|
|
$("TotalPeerConnections").textContent = serverState.total_peer_connections;
|
|
$("ReadCacheHits").textContent = serverState.read_cache_hits + "%";
|
|
$("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false);
|
|
$("WriteCacheOverload").textContent = serverState.write_cache_overload + "%";
|
|
$("ReadCacheOverload").textContent = serverState.read_cache_overload + "%";
|
|
$("QueuedIOJobs").textContent = serverState.queued_io_jobs;
|
|
$("AverageTimeInQueue").textContent = serverState.average_time_queue + " ms";
|
|
$("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false);
|
|
}
|
|
|
|
switch (serverState.connection_status) {
|
|
case "connected":
|
|
$("connectionStatus").src = "images/connected.svg";
|
|
$("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
|
|
$("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
|
|
break;
|
|
case "firewalled":
|
|
$("connectionStatus").src = "images/firewalled.svg";
|
|
$("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
|
|
$("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
|
|
break;
|
|
default:
|
|
$("connectionStatus").src = "images/disconnected.svg";
|
|
$("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
|
|
$("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
|
|
break;
|
|
}
|
|
|
|
if (queueing_enabled !== serverState.queueing) {
|
|
queueing_enabled = serverState.queueing;
|
|
torrentsTable.columns["priority"].force_hide = !queueing_enabled;
|
|
torrentsTable.updateColumn("priority");
|
|
if (queueing_enabled) {
|
|
$("topQueuePosItem").removeClass("invisible");
|
|
$("increaseQueuePosItem").removeClass("invisible");
|
|
$("decreaseQueuePosItem").removeClass("invisible");
|
|
$("bottomQueuePosItem").removeClass("invisible");
|
|
$("queueingButtons").removeClass("invisible");
|
|
$("queueingMenuItems").removeClass("invisible");
|
|
}
|
|
else {
|
|
$("topQueuePosItem").addClass("invisible");
|
|
$("increaseQueuePosItem").addClass("invisible");
|
|
$("decreaseQueuePosItem").addClass("invisible");
|
|
$("bottomQueuePosItem").addClass("invisible");
|
|
$("queueingButtons").addClass("invisible");
|
|
$("queueingMenuItems").addClass("invisible");
|
|
}
|
|
}
|
|
|
|
if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
|
|
alternativeSpeedLimits = serverState.use_alt_speed_limits;
|
|
updateAltSpeedIcon(alternativeSpeedLimits);
|
|
}
|
|
|
|
if (useSubcategories !== serverState.use_subcategories) {
|
|
useSubcategories = serverState.use_subcategories;
|
|
updateCategoryList();
|
|
}
|
|
|
|
serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
|
|
};
|
|
|
|
const updateAltSpeedIcon = function(enabled) {
|
|
if (enabled) {
|
|
$("alternativeSpeedLimits").src = "images/slow.svg";
|
|
$("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
|
|
$("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
|
|
}
|
|
else {
|
|
$("alternativeSpeedLimits").src = "images/slow_off.svg";
|
|
$("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
|
|
$("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
|
|
}
|
|
};
|
|
|
|
$("alternativeSpeedLimits").addEventListener("click", () => {
|
|
// Change icon immediately to give some feedback
|
|
updateAltSpeedIcon(!alternativeSpeedLimits);
|
|
|
|
new Request({
|
|
url: "api/v2/transfer/toggleSpeedLimitsMode",
|
|
method: "post",
|
|
onComplete: function() {
|
|
alternativeSpeedLimits = !alternativeSpeedLimits;
|
|
updateMainData();
|
|
},
|
|
onFailure: function() {
|
|
// Restore icon in case of failure
|
|
updateAltSpeedIcon(alternativeSpeedLimits);
|
|
}
|
|
}).send();
|
|
});
|
|
|
|
$("DlInfos").addEventListener("click", () => { globalDownloadLimitFN(); });
|
|
$("UpInfos").addEventListener("click", () => { globalUploadLimitFN(); });
|
|
|
|
$("showTopToolbarLink").addEventListener("click", (e) => {
|
|
showTopToolbar = !showTopToolbar;
|
|
LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
|
|
if (showTopToolbar) {
|
|
$("showTopToolbarLink").firstChild.style.opacity = "1";
|
|
$("mochaToolbar").removeClass("invisible");
|
|
}
|
|
else {
|
|
$("showTopToolbarLink").firstChild.style.opacity = "0";
|
|
$("mochaToolbar").addClass("invisible");
|
|
}
|
|
MochaUI.Desktop.setDesktopSize();
|
|
});
|
|
|
|
$("showStatusBarLink").addEventListener("click", (e) => {
|
|
showStatusBar = !showStatusBar;
|
|
LocalPreferences.set("show_status_bar", showStatusBar.toString());
|
|
if (showStatusBar) {
|
|
$("showStatusBarLink").firstChild.style.opacity = "1";
|
|
$("desktopFooterWrapper").removeClass("invisible");
|
|
}
|
|
else {
|
|
$("showStatusBarLink").firstChild.style.opacity = "0";
|
|
$("desktopFooterWrapper").addClass("invisible");
|
|
}
|
|
MochaUI.Desktop.setDesktopSize();
|
|
});
|
|
|
|
const registerMagnetHandler = function() {
|
|
if (typeof navigator.registerProtocolHandler !== "function") {
|
|
if (window.location.protocol !== "https:")
|
|
alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
|
|
else
|
|
alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
|
|
return;
|
|
}
|
|
|
|
const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
|
|
const hashParams = new URLSearchParams(hashString);
|
|
hashParams.set("download", "");
|
|
|
|
const templateHashString = hashParams.toString().replace("download=", "download=%s");
|
|
const templateUrl = location.origin + location.pathname
|
|
+ location.search + "#" + templateHashString;
|
|
|
|
navigator.registerProtocolHandler("magnet", templateUrl,
|
|
"qBittorrent WebUI magnet handler");
|
|
};
|
|
$("registerMagnetHandlerLink").addEventListener("click", (e) => {
|
|
registerMagnetHandler();
|
|
});
|
|
|
|
$("showFiltersSidebarLink").addEventListener("click", (e) => {
|
|
showFiltersSidebar = !showFiltersSidebar;
|
|
LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
|
|
if (showFiltersSidebar) {
|
|
$("showFiltersSidebarLink").firstChild.style.opacity = "1";
|
|
$("filtersColumn").removeClass("invisible");
|
|
$("filtersColumn_handle").removeClass("invisible");
|
|
}
|
|
else {
|
|
$("showFiltersSidebarLink").firstChild.style.opacity = "0";
|
|
$("filtersColumn").addClass("invisible");
|
|
$("filtersColumn_handle").addClass("invisible");
|
|
}
|
|
MochaUI.Desktop.setDesktopSize();
|
|
});
|
|
|
|
$("speedInBrowserTitleBarLink").addEventListener("click", (e) => {
|
|
speedInTitle = !speedInTitle;
|
|
LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
|
|
if (speedInTitle)
|
|
$("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
|
|
else
|
|
$("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
|
|
processServerState();
|
|
});
|
|
|
|
$("showSearchEngineLink").addEventListener("click", (e) => {
|
|
window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
|
|
LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
|
|
updateTabDisplay();
|
|
});
|
|
|
|
$("showRssReaderLink").addEventListener("click", (e) => {
|
|
window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
|
|
LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
|
|
updateTabDisplay();
|
|
});
|
|
|
|
$("showLogViewerLink").addEventListener("click", (e) => {
|
|
window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
|
|
LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
|
|
updateTabDisplay();
|
|
});
|
|
|
|
const updateTabDisplay = function() {
|
|
if (window.qBittorrent.Client.isShowRssReader()) {
|
|
$("showRssReaderLink").firstChild.style.opacity = "1";
|
|
$("mainWindowTabs").removeClass("invisible");
|
|
$("rssTabLink").removeClass("invisible");
|
|
if (!MochaUI.Panels.instances.RssPanel)
|
|
addRssPanel();
|
|
}
|
|
else {
|
|
$("showRssReaderLink").firstChild.style.opacity = "0";
|
|
$("rssTabLink").addClass("invisible");
|
|
if ($("rssTabLink").hasClass("selected"))
|
|
$("transfersTabLink").click();
|
|
}
|
|
|
|
if (window.qBittorrent.Client.isShowSearchEngine()) {
|
|
$("showSearchEngineLink").firstChild.style.opacity = "1";
|
|
$("mainWindowTabs").removeClass("invisible");
|
|
$("searchTabLink").removeClass("invisible");
|
|
if (!MochaUI.Panels.instances.SearchPanel)
|
|
addSearchPanel();
|
|
}
|
|
else {
|
|
$("showSearchEngineLink").firstChild.style.opacity = "0";
|
|
$("searchTabLink").addClass("invisible");
|
|
if ($("searchTabLink").hasClass("selected"))
|
|
$("transfersTabLink").click();
|
|
}
|
|
|
|
if (window.qBittorrent.Client.isShowLogViewer()) {
|
|
$("showLogViewerLink").firstChild.style.opacity = "1";
|
|
$("mainWindowTabs").removeClass("invisible");
|
|
$("logTabLink").removeClass("invisible");
|
|
if (!MochaUI.Panels.instances.LogPanel)
|
|
addLogPanel();
|
|
}
|
|
else {
|
|
$("showLogViewerLink").firstChild.style.opacity = "0";
|
|
$("logTabLink").addClass("invisible");
|
|
if ($("logTabLink").hasClass("selected"))
|
|
$("transfersTabLink").click();
|
|
}
|
|
|
|
// display no tabs
|
|
if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
|
|
$("mainWindowTabs").addClass("invisible");
|
|
};
|
|
|
|
$("StatisticsLink").addEventListener("click", () => { StatisticsLinkFN(); });
|
|
|
|
// main window tabs
|
|
|
|
const showTransfersTab = function() {
|
|
const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
|
|
if (showFiltersSidebar) {
|
|
$("filtersColumn").removeClass("invisible");
|
|
$("filtersColumn_handle").removeClass("invisible");
|
|
}
|
|
$("mainColumn").removeClass("invisible");
|
|
$("torrentsFilterToolbar").removeClass("invisible");
|
|
|
|
customSyncMainDataInterval = null;
|
|
syncData(100);
|
|
|
|
hideSearchTab();
|
|
hideRssTab();
|
|
hideLogTab();
|
|
|
|
LocalPreferences.set("selected_window_tab", "transfers");
|
|
};
|
|
|
|
const hideTransfersTab = function() {
|
|
$("filtersColumn").addClass("invisible");
|
|
$("filtersColumn_handle").addClass("invisible");
|
|
$("mainColumn").addClass("invisible");
|
|
$("torrentsFilterToolbar").addClass("invisible");
|
|
MochaUI.Desktop.resizePanels();
|
|
};
|
|
|
|
const showSearchTab = (function() {
|
|
let searchTabInitialized = false;
|
|
|
|
return () => {
|
|
// we must wait until the panel is fully loaded before proceeding.
|
|
// this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
|
|
// MochaUI loads these files asynchronously and thus all required libs may not be available immediately
|
|
if (!isSearchPanelLoaded) {
|
|
setTimeout(() => {
|
|
showSearchTab();
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
if (!searchTabInitialized) {
|
|
window.qBittorrent.Search.init();
|
|
searchTabInitialized = true;
|
|
}
|
|
|
|
$("searchTabColumn").removeClass("invisible");
|
|
customSyncMainDataInterval = 30000;
|
|
hideTransfersTab();
|
|
hideRssTab();
|
|
hideLogTab();
|
|
|
|
LocalPreferences.set("selected_window_tab", "search");
|
|
};
|
|
})();
|
|
|
|
const hideSearchTab = function() {
|
|
$("searchTabColumn").addClass("invisible");
|
|
MochaUI.Desktop.resizePanels();
|
|
};
|
|
|
|
const showRssTab = (function() {
|
|
let rssTabInitialized = false;
|
|
|
|
return () => {
|
|
if (!rssTabInitialized) {
|
|
window.qBittorrent.Rss.init();
|
|
rssTabInitialized = true;
|
|
}
|
|
else {
|
|
window.qBittorrent.Rss.load();
|
|
}
|
|
|
|
$("rssTabColumn").removeClass("invisible");
|
|
customSyncMainDataInterval = 30000;
|
|
hideTransfersTab();
|
|
hideSearchTab();
|
|
hideLogTab();
|
|
|
|
LocalPreferences.set("selected_window_tab", "rss");
|
|
};
|
|
})();
|
|
|
|
const hideRssTab = function() {
|
|
$("rssTabColumn").addClass("invisible");
|
|
window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
|
|
MochaUI.Desktop.resizePanels();
|
|
};
|
|
|
|
const showLogTab = (function() {
|
|
let logTabInitialized = false;
|
|
|
|
return () => {
|
|
// we must wait until the panel is fully loaded before proceeding.
|
|
// this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
|
|
// MochaUI loads these files asynchronously and thus all required libs may not be available immediately
|
|
if (!isLogPanelLoaded) {
|
|
setTimeout(() => {
|
|
showLogTab();
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
if (!logTabInitialized) {
|
|
window.qBittorrent.Log.init();
|
|
logTabInitialized = true;
|
|
}
|
|
else {
|
|
window.qBittorrent.Log.load();
|
|
}
|
|
|
|
$("logTabColumn").removeClass("invisible");
|
|
customSyncMainDataInterval = 30000;
|
|
hideTransfersTab();
|
|
hideSearchTab();
|
|
hideRssTab();
|
|
|
|
LocalPreferences.set("selected_window_tab", "log");
|
|
};
|
|
})();
|
|
|
|
const hideLogTab = function() {
|
|
$("logTabColumn").addClass("invisible");
|
|
MochaUI.Desktop.resizePanels();
|
|
window.qBittorrent.Log && window.qBittorrent.Log.unload();
|
|
};
|
|
|
|
const addSearchPanel = function() {
|
|
new MochaUI.Panel({
|
|
id: "SearchPanel",
|
|
title: "Search",
|
|
header: false,
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
loadMethod: "xhr",
|
|
contentURL: "views/search.html",
|
|
require: {
|
|
js: ["scripts/search.js"],
|
|
onload: () => {
|
|
isSearchPanelLoaded = true;
|
|
},
|
|
},
|
|
content: "",
|
|
column: "searchTabColumn",
|
|
height: null
|
|
});
|
|
};
|
|
|
|
const addRssPanel = function() {
|
|
new MochaUI.Panel({
|
|
id: "RssPanel",
|
|
title: "Rss",
|
|
header: false,
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
loadMethod: "xhr",
|
|
contentURL: "views/rss.html",
|
|
content: "",
|
|
column: "rssTabColumn",
|
|
height: null
|
|
});
|
|
};
|
|
|
|
const addLogPanel = function() {
|
|
new MochaUI.Panel({
|
|
id: "LogPanel",
|
|
title: "Log",
|
|
header: true,
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
loadMethod: "xhr",
|
|
contentURL: "views/log.html",
|
|
require: {
|
|
css: ["css/vanillaSelectBox.css"],
|
|
js: ["scripts/lib/vanillaSelectBox.js"],
|
|
onload: () => {
|
|
isLogPanelLoaded = true;
|
|
},
|
|
},
|
|
tabsURL: "views/logTabs.html",
|
|
tabsOnload: function() {
|
|
MochaUI.initializeTabs("panelTabs");
|
|
|
|
$("logMessageLink").addEventListener("click", (e) => {
|
|
window.qBittorrent.Log.setCurrentTab("main");
|
|
});
|
|
|
|
$("logPeerLink").addEventListener("click", (e) => {
|
|
window.qBittorrent.Log.setCurrentTab("peer");
|
|
});
|
|
},
|
|
collapsible: false,
|
|
content: "",
|
|
column: "logTabColumn",
|
|
height: null
|
|
});
|
|
};
|
|
|
|
const handleDownloadParam = function() {
|
|
// Extract torrent URL from download param in WebUI URL hash
|
|
const downloadHash = "#download=";
|
|
if (location.hash.indexOf(downloadHash) !== 0)
|
|
return;
|
|
|
|
const url = decodeURIComponent(location.hash.substring(downloadHash.length));
|
|
// Remove the processed hash from the URL
|
|
history.replaceState("", document.title, (location.pathname + location.search));
|
|
showDownloadPage([url]);
|
|
};
|
|
|
|
new MochaUI.Panel({
|
|
id: "transferList",
|
|
title: "Panel",
|
|
header: false,
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
loadMethod: "xhr",
|
|
contentURL: "views/transferlist.html",
|
|
onContentLoaded: function() {
|
|
handleDownloadParam();
|
|
updateMainData();
|
|
},
|
|
column: "mainColumn",
|
|
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
|
saveColumnSizes();
|
|
}),
|
|
height: null
|
|
});
|
|
let prop_h = LocalPreferences.get("properties_height_rel");
|
|
if (prop_h !== null)
|
|
prop_h = prop_h.toFloat() * Window.getSize().y;
|
|
else
|
|
prop_h = Window.getSize().y / 2.0;
|
|
new MochaUI.Panel({
|
|
id: "propertiesPanel",
|
|
title: "Panel",
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
contentURL: "views/properties.html",
|
|
require: {
|
|
js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
|
|
onload: function() {
|
|
updatePropertiesPanel = function() {
|
|
switch (LocalPreferences.get("selected_properties_tab")) {
|
|
case "propGeneralLink":
|
|
window.qBittorrent.PropGeneral.updateData();
|
|
break;
|
|
case "propTrackersLink":
|
|
window.qBittorrent.PropTrackers.updateData();
|
|
break;
|
|
case "propPeersLink":
|
|
window.qBittorrent.PropPeers.updateData();
|
|
break;
|
|
case "propWebSeedsLink":
|
|
window.qBittorrent.PropWebseeds.updateData();
|
|
break;
|
|
case "propFilesLink":
|
|
window.qBittorrent.PropFiles.updateData();
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
},
|
|
tabsURL: "views/propertiesToolbar.html",
|
|
tabsOnload: function() {}, // must be included, otherwise panel won't load properly
|
|
onContentLoaded: function() {
|
|
this.panelHeaderCollapseBoxEl.classList.add("invisible");
|
|
|
|
const togglePropertiesPanel = () => {
|
|
this.collapseToggleEl.click();
|
|
LocalPreferences.set("properties_panel_collapsed", this.isCollapsed.toString());
|
|
};
|
|
|
|
const selectTab = (tabID) => {
|
|
const isAlreadySelected = this.panelHeaderEl.getElementById(tabID).classList.contains("selected");
|
|
if (!isAlreadySelected) {
|
|
for (const tab of this.panelHeaderEl.getElementById("propertiesTabs").children)
|
|
tab.classList.toggle("selected", tab.id === tabID);
|
|
|
|
const tabContentID = tabID.replace("Link", "");
|
|
for (const tabContent of this.contentEl.children)
|
|
tabContent.classList.toggle("invisible", tabContent.id !== tabContentID);
|
|
|
|
LocalPreferences.set("selected_properties_tab", tabID);
|
|
}
|
|
|
|
if (isAlreadySelected || this.isCollapsed)
|
|
togglePropertiesPanel();
|
|
};
|
|
|
|
const lastUsedTab = LocalPreferences.get("selected_properties_tab", "propGeneralLink");
|
|
selectTab(lastUsedTab);
|
|
|
|
const startCollapsed = LocalPreferences.get("properties_panel_collapsed", "false") === "true";
|
|
if (startCollapsed)
|
|
togglePropertiesPanel();
|
|
|
|
this.panelHeaderContentEl.addEventListener("click", (e) => {
|
|
const selectedTab = e.target.closest("li");
|
|
if (!selectedTab)
|
|
return;
|
|
|
|
selectTab(selectedTab.id);
|
|
updatePropertiesPanel();
|
|
|
|
const showFilesFilter = (selectedTab.id === "propFilesLink") && !this.isCollapsed;
|
|
document.getElementById("torrentFilesFilterToolbar").classList.toggle("invisible", !showFilesFilter);
|
|
});
|
|
},
|
|
column: "mainColumn",
|
|
height: prop_h
|
|
});
|
|
|
|
// listen for changes to torrentsFilterInput
|
|
let torrentsFilterInputTimer = -1;
|
|
$("torrentsFilterInput").addEventListener("input", () => {
|
|
clearTimeout(torrentsFilterInputTimer);
|
|
torrentsFilterInputTimer = setTimeout(() => {
|
|
torrentsFilterInputTimer = -1;
|
|
torrentsTable.updateTable();
|
|
}, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
|
|
});
|
|
|
|
document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { torrentsTable.updateTable(); });
|
|
|
|
$("transfersTabLink").addEventListener("click", () => { showTransfersTab(); });
|
|
$("searchTabLink").addEventListener("click", () => { showSearchTab(); });
|
|
$("rssTabLink").addEventListener("click", () => { showRssTab(); });
|
|
$("logTabLink").addEventListener("click", () => { showLogTab(); });
|
|
updateTabDisplay();
|
|
|
|
const registerDragAndDrop = () => {
|
|
$("desktop").addEventListener("dragover", (ev) => {
|
|
if (ev.preventDefault)
|
|
ev.preventDefault();
|
|
});
|
|
|
|
$("desktop").addEventListener("dragenter", (ev) => {
|
|
if (ev.preventDefault)
|
|
ev.preventDefault();
|
|
});
|
|
|
|
$("desktop").addEventListener("drop", (ev) => {
|
|
if (ev.preventDefault)
|
|
ev.preventDefault();
|
|
|
|
const droppedFiles = ev.dataTransfer.files;
|
|
|
|
if (droppedFiles.length > 0) {
|
|
// dropped files or folders
|
|
|
|
// can't handle folder due to cannot put the filelist (from dropped folder)
|
|
// to <input> `files` field
|
|
for (const item of ev.dataTransfer.items) {
|
|
if (item.webkitGetAsEntry().isDirectory)
|
|
return;
|
|
}
|
|
|
|
const id = "uploadPage";
|
|
new MochaUI.Window({
|
|
id: id,
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
|
|
loadMethod: "iframe",
|
|
contentURL: new URI("upload.html").toString(),
|
|
addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
|
|
scrollbars: true,
|
|
maximizable: false,
|
|
paddingVertical: 0,
|
|
paddingHorizontal: 0,
|
|
width: loadWindowWidth(id, 500),
|
|
height: loadWindowHeight(id, 460),
|
|
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
|
saveWindowSize(id);
|
|
}),
|
|
onContentLoaded: () => {
|
|
const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
|
|
fileInput.files = droppedFiles;
|
|
}
|
|
});
|
|
}
|
|
|
|
const droppedText = ev.dataTransfer.getData("text");
|
|
if (droppedText.length > 0) {
|
|
// dropped text
|
|
|
|
const urls = droppedText.split("\n")
|
|
.map((str) => str.trim())
|
|
.filter((str) => {
|
|
const lowercaseStr = str.toLowerCase();
|
|
return lowercaseStr.startsWith("http:")
|
|
|| lowercaseStr.startsWith("https:")
|
|
|| lowercaseStr.startsWith("magnet:")
|
|
|| ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
|
|
|| ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
|
|
});
|
|
|
|
if (urls.length <= 0)
|
|
return;
|
|
|
|
const id = "downloadPage";
|
|
const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
|
|
new MochaUI.Window({
|
|
id: id,
|
|
icon: "images/qbittorrent-tray.svg",
|
|
title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
|
|
loadMethod: "iframe",
|
|
contentURL: contentURI.toString(),
|
|
addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
|
|
scrollbars: true,
|
|
maximizable: false,
|
|
closable: true,
|
|
paddingVertical: 0,
|
|
paddingHorizontal: 0,
|
|
width: loadWindowWidth(id, 500),
|
|
height: loadWindowHeight(id, 600),
|
|
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
|
|
saveWindowSize(id);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
};
|
|
registerDragAndDrop();
|
|
|
|
new Keyboard({
|
|
defaultEventType: "keydown",
|
|
events: {
|
|
"ctrl+a": function(event) {
|
|
if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
|
|
return;
|
|
if (event.target.isContentEditable)
|
|
return;
|
|
torrentsTable.selectAll();
|
|
event.preventDefault();
|
|
},
|
|
"delete": function(event) {
|
|
if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
|
|
return;
|
|
if (event.target.isContentEditable)
|
|
return;
|
|
deleteFN();
|
|
event.preventDefault();
|
|
},
|
|
"shift+delete": (event) => {
|
|
if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
|
|
return;
|
|
if (event.target.isContentEditable)
|
|
return;
|
|
deleteFN(true);
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
}).activate();
|
|
});
|
|
|
|
window.addEventListener("load", () => {
|
|
// fetch various data and store it in memory
|
|
window.qBittorrent.Cache.buildInfo.init();
|
|
window.qBittorrent.Cache.preferences.init();
|
|
window.qBittorrent.Cache.qbtVersion.init();
|
|
|
|
// switch to previously used tab
|
|
const previouslyUsedTab = LocalPreferences.get("selected_window_tab", "transfers");
|
|
switch (previouslyUsedTab) {
|
|
case "search":
|
|
if (window.qBittorrent.Client.isShowSearchEngine())
|
|
$("searchTabLink").click();
|
|
break;
|
|
case "rss":
|
|
if (window.qBittorrent.Client.isShowRssReader())
|
|
$("rssTabLink").click();
|
|
break;
|
|
case "log":
|
|
if (window.qBittorrent.Client.isShowLogViewer())
|
|
$("logTabLink").click();
|
|
break;
|
|
case "transfers":
|
|
$("transfersTabLink").click();
|
|
break;
|
|
default:
|
|
console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
|
|
$("transfersTabLink").click();
|
|
break;
|
|
};
|
|
});
|