qBittorrent/src/webui/www/private/scripts/contextmenu.js
2024-05-27 22:50:17 +08:00

691 lines
27 KiB
JavaScript

/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2009 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
"use strict";
if (window.qBittorrent === undefined) {
window.qBittorrent = {};
}
window.qBittorrent.ContextMenu = (function() {
const exports = function() {
return {
ContextMenu: ContextMenu,
TorrentsTableContextMenu: TorrentsTableContextMenu,
CategoriesFilterContextMenu: CategoriesFilterContextMenu,
TagsFilterContextMenu: TagsFilterContextMenu,
SearchPluginsTableContextMenu: SearchPluginsTableContextMenu,
RssFeedContextMenu: RssFeedContextMenu,
RssArticleContextMenu: RssArticleContextMenu,
RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu
};
};
let lastShownContextMenu = null;
const ContextMenu = new Class({
//implements
Implements: [Options, Events],
//options
options: {
actions: {},
menu: "menu_id",
stopEvent: true,
targets: "body",
offsets: {
x: 0,
y: 0
},
onShow: () => {},
onHide: () => {},
onClick: () => {},
fadeSpeed: 200,
touchTimer: 600
},
//initialization
initialize: function(options) {
//set options
this.setOptions(options);
//option diffs menu
this.menu = $(this.options.menu);
this.targets = $$(this.options.targets);
//fx
this.fx = new Fx.Tween(this.menu, {
property: "opacity",
duration: this.options.fadeSpeed,
onComplete: function() {
if (this.getStyle("opacity")) {
this.setStyle("visibility", "visible");
}
else {
this.setStyle("visibility", "hidden");
}
}.bind(this.menu)
});
//hide and begin the listener
this.hide().startListener();
//hide the menu
this.menu.setStyles({
"position": "absolute",
"top": "-900000px",
"display": "block"
});
},
adjustMenuPosition: function(e) {
this.updateMenuItems();
const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75;
if (this.menu.hasClass("scrollableMenu"))
this.menu.setStyle("max-height", scrollableMenuMaxHeight);
// draw the menu off-screen to know the menu dimensions
this.menu.setStyles({
left: "-999em",
top: "-999em"
});
// position the menu
let xPosMenu = e.page.x + this.options.offsets.x;
let yPosMenu = e.page.y + this.options.offsets.y;
if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
xPosMenu -= this.menu.offsetWidth;
if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
if (xPosMenu < 0)
xPosMenu = 0;
if (yPosMenu < 0)
yPosMenu = 0;
this.menu.setStyles({
left: xPosMenu,
top: yPosMenu,
position: "absolute",
"z-index": "2000"
});
// position the sub-menu
const uls = this.menu.getElementsByTagName("ul");
for (let i = 0; i < uls.length; ++i) {
const ul = uls[i];
if (ul.hasClass("scrollableMenu"))
ul.setStyle("max-height", scrollableMenuMaxHeight);
const rectParent = ul.parentNode.getBoundingClientRect();
const xPosOrigin = rectParent.left;
const yPosOrigin = rectParent.bottom;
let xPos = xPosOrigin + rectParent.width - 1;
let yPos = yPosOrigin - rectParent.height - 1;
if ((xPos + ul.offsetWidth) > document.documentElement.clientWidth)
xPos -= (ul.offsetWidth + rectParent.width - 2);
if ((yPos + ul.offsetHeight) > document.documentElement.clientHeight)
yPos = document.documentElement.clientHeight - ul.offsetHeight;
if (xPos < 0)
xPos = 0;
if (yPos < 0)
yPos = 0;
ul.setStyles({
"margin-left": xPos - xPosOrigin,
"margin-top": yPos - yPosOrigin
});
}
},
setupEventListeners: function(elem) {
elem.addEvent("contextmenu", function(e) {
this.triggerMenu(e, elem);
}.bind(this));
elem.addEvent("click", function(e) {
this.hide();
}.bind(this));
elem.addEvent("touchstart", function(e) {
this.hide();
this.touchStartAt = performance.now();
this.touchStartEvent = e;
}.bind(this));
elem.addEvent("touchend", function(e) {
const now = performance.now();
const touchStartAt = this.touchStartAt;
const touchStartEvent = this.touchStartEvent;
this.touchStartAt = null;
this.touchStartEvent = null;
const isTargetUnchanged = (Math.abs(e.event.pageX - touchStartEvent.event.pageX) <= 10) && (Math.abs(e.event.pageY - touchStartEvent.event.pageY) <= 10);
if (((now - touchStartAt) >= this.options.touchTimer) && isTargetUnchanged) {
this.triggerMenu(touchStartEvent, elem);
}
}.bind(this));
},
addTarget: function(t) {
// prevent long press from selecting this text
t.style.setProperty("user-select", "none");
t.style.setProperty("-webkit-user-select", "none");
this.targets[this.targets.length] = t;
this.setupEventListeners(t);
},
triggerMenu: function(e, el) {
if (this.options.disabled)
return;
//prevent default, if told to
if (this.options.stopEvent) {
e.stop();
}
//record this as the trigger
this.options.element = $(el);
this.adjustMenuPosition(e);
//show the menu
this.show();
},
//get things started
startListener: function() {
/* all elements */
this.targets.each(function(el) {
this.setupEventListeners(el);
}.bind(this), this);
/* menu items */
this.menu.getElements("a").each(function(item) {
item.addEvent("click", function(e) {
e.preventDefault();
if (!item.hasClass("disabled")) {
this.execute(item.get("href").split("#")[1], $(this.options.element));
this.fireEvent("click", [item, e]);
}
}.bind(this));
}, this);
//hide on body click
$(document.body).addEvent("click", function() {
this.hide();
}.bind(this));
},
updateMenuItems: function() {},
//show menu
show: function(trigger) {
if (lastShownContextMenu && (lastShownContextMenu !== this))
lastShownContextMenu.hide();
this.fx.start(1);
this.fireEvent("show");
lastShownContextMenu = this;
return this;
},
//hide the menu
hide: function(trigger) {
if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
this.fx.start(0);
//this.menu.fade('out');
this.fireEvent("hide");
}
return this;
},
setItemChecked: function(item, checked) {
this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity =
checked ? "1" : "0";
return this;
},
getItemChecked: function(item) {
return this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity !== "0";
},
//hide an item
hideItem: function(item) {
this.menu.getElement("a[href$=" + item + "]").parentNode.addClass("invisible");
return this;
},
//show an item
showItem: function(item) {
this.menu.getElement("a[href$=" + item + "]").parentNode.removeClass("invisible");
return this;
},
//disable the entire menu
disable: function() {
this.options.disabled = true;
return this;
},
//enable the entire menu
enable: function() {
this.options.disabled = false;
return this;
},
//execute an action
execute: function(action, element) {
if (this.options.actions[action]) {
this.options.actions[action](element, this, action);
}
return this;
}
});
const TorrentsTableContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
let all_are_seq_dl = true;
let there_are_seq_dl = false;
let all_are_f_l_piece_prio = true;
let there_are_f_l_piece_prio = false;
let all_are_downloaded = true;
let all_are_stopped = true;
let there_are_stopped = false;
let all_are_force_start = true;
let there_are_force_start = false;
let all_are_super_seeding = true;
let all_are_auto_tmm = true;
let there_are_auto_tmm = false;
const tagCount = new Map();
const selectedRows = torrentsTable.selectedRowsIds();
selectedRows.forEach((item, index) => {
const data = torrentsTable.rows.get(item).full_data;
if (data["seq_dl"] !== true)
all_are_seq_dl = false;
else
there_are_seq_dl = true;
if (data["f_l_piece_prio"] !== true)
all_are_f_l_piece_prio = false;
else
there_are_f_l_piece_prio = true;
if (data["progress"] !== 1.0) // not downloaded
all_are_downloaded = false;
else if (data["super_seeding"] !== true)
all_are_super_seeding = false;
if ((data["state"] !== "stoppedUP") && (data["state"] !== "stoppedDL"))
all_are_stopped = false;
else
there_are_stopped = true;
if (data["force_start"] !== true)
all_are_force_start = false;
else
there_are_force_start = true;
if (data["auto_tmm"] === true)
there_are_auto_tmm = true;
else
all_are_auto_tmm = false;
const torrentTags = data["tags"].split(", ");
for (const tag of torrentTags) {
const count = tagCount.get(tag);
tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
}
});
// hide renameFiles when more than 1 torrent is selected
if (selectedRows.length === 1) {
const data = torrentsTable.rows.get(selectedRows[0]).full_data;
let metadata_downloaded = !((data["state"] === "metaDL") || (data["state"] === "forcedMetaDL") || (data["total_size"] === -1));
// hide renameFiles when metadata hasn't been downloaded yet
metadata_downloaded
? this.showItem("renameFiles")
: this.hideItem("renameFiles");
}
else {
this.hideItem("renameFiles");
}
if (all_are_downloaded) {
this.hideItem("downloadLimit");
this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
this.hideItem("sequentialDownload");
this.hideItem("firstLastPiecePrio");
this.showItem("superSeeding");
this.setItemChecked("superSeeding", all_are_super_seeding);
}
else {
const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
if (!show_seq_dl && show_f_l_piece_prio)
this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
else
this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
if (show_seq_dl)
this.showItem("sequentialDownload");
else
this.hideItem("sequentialDownload");
if (show_f_l_piece_prio)
this.showItem("firstLastPiecePrio");
else
this.hideItem("firstLastPiecePrio");
this.setItemChecked("sequentialDownload", all_are_seq_dl);
this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
this.showItem("downloadLimit");
this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
this.hideItem("superSeeding");
}
this.showItem("start");
this.showItem("stop");
this.showItem("forceStart");
if (all_are_stopped)
this.hideItem("stop");
else if (all_are_force_start)
this.hideItem("forceStart");
else if (!there_are_stopped && !there_are_force_start)
this.hideItem("start");
if (!all_are_auto_tmm && there_are_auto_tmm) {
this.hideItem("autoTorrentManagement");
}
else {
this.showItem("autoTorrentManagement");
this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
}
const contextTagList = $("contextTagList");
tagList.forEach((tag, tagHash) => {
const checkbox = contextTagList.getElement(`a[href="#Tag/${tagHash}"] input[type="checkbox"]`);
const count = tagCount.get(tag.name);
const hasCount = (count !== undefined);
const isLesser = (count < selectedRows.length);
checkbox.indeterminate = (hasCount ? isLesser : false);
checkbox.checked = (hasCount ? !isLesser : false);
});
},
updateCategoriesSubMenu: function(categoryList) {
const contextCategoryList = $("contextCategoryList");
contextCategoryList.getChildren().each(c => c.destroy());
contextCategoryList.appendChild(new Element("li", {
html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>'
}));
contextCategoryList.appendChild(new Element("li", {
html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>'
}));
const sortedCategories = [];
categoryList.forEach((category, hash) => sortedCategories.push({
categoryName: category.name,
categoryHash: hash
}));
sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
left.categoryName, right.categoryName));
let first = true;
for (const { categoryName, categoryHash } of sortedCategories) {
const el = new Element("li", {
html: `<a href="javascript:torrentSetCategoryFN(${categoryHash});"><img src="images/view-categories.svg"/>${window.qBittorrent.Misc.escapeHtml(categoryName)}</a>`
});
if (first) {
el.addClass("separator");
first = false;
}
contextCategoryList.appendChild(el);
}
},
updateTagsSubMenu: function(tagList) {
const contextTagList = $("contextTagList");
while (contextTagList.firstChild !== null)
contextTagList.removeChild(contextTagList.firstChild);
contextTagList.appendChild(new Element("li", {
html: '<a href="javascript:torrentAddTagsFN();">'
+ '<img src="images/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>'
+ " QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"
+ "</a>"
}));
contextTagList.appendChild(new Element("li", {
html: '<a href="javascript:torrentRemoveAllTagsFN();">'
+ '<img src="images/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>'
+ " QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"
+ "</a>"
}));
const sortedTags = [];
tagList.forEach((tag, hash) => sortedTags.push({
tagName: tag.name,
tagHash: hash
}));
sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
for (let i = 0; i < sortedTags.length; ++i) {
const { tagName, tagHash } = sortedTags[i];
const el = new Element("li", {
html: `<a href="#Tag/${tagHash}" onclick="event.preventDefault(); torrentSetTagsFN(${tagHash}, !event.currentTarget.getElement('input[type=checkbox]').checked);">`
+ '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + window.qBittorrent.Misc.escapeHtml(tagName)
+ "</a>"
});
if (i === 0)
el.addClass("separator");
contextTagList.appendChild(el);
}
}
});
const CategoriesFilterContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
const id = Number(this.options.element.id);
if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
this.showItem("editCategory");
this.showItem("deleteCategory");
if (useSubcategories) {
this.showItem("createSubcategory");
}
else {
this.hideItem("createSubcategory");
}
}
else {
this.hideItem("editCategory");
this.hideItem("deleteCategory");
this.hideItem("createSubcategory");
}
}
});
const TagsFilterContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
const id = Number(this.options.element.id);
if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
this.showItem("deleteTag");
else
this.hideItem("deleteTag");
}
});
const SearchPluginsTableContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
const enabledColumnIndex = function(text) {
const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
for (let i = 0; i < columns.length; ++i)
if (columns[i].get("html") === "Enabled")
return i;
};
this.showItem("Enabled");
this.setItemChecked("Enabled", this.options.element.getChildren("td")[enabledColumnIndex()].get("html") === "Yes");
this.showItem("Uninstall");
}
});
const RssFeedContextMenu = new Class({
Extends: ContextMenu,
updateMenuItems: function() {
let selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
switch (selectedRows.length) {
case 0:
// remove separator on top of newSubscription entry to avoid double line
this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
// menu when nothing selected
this.hideItem("update");
this.hideItem("markRead");
this.hideItem("rename");
this.hideItem("delete");
this.showItem("newSubscription");
this.showItem("newFolder");
this.showItem("updateAll");
this.hideItem("copyFeedURL");
break;
case 1:
if (selectedRows[0] === 0) {
// menu when "unread" feed selected
this.showItem("update");
this.showItem("markRead");
this.hideItem("rename");
this.hideItem("delete");
this.showItem("newSubscription");
this.hideItem("newFolder");
this.hideItem("updateAll");
this.hideItem("copyFeedURL");
}
else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === "") {
// menu when single folder selected
this.showItem("update");
this.showItem("markRead");
this.showItem("rename");
this.showItem("delete");
this.showItem("newSubscription");
this.showItem("newFolder");
this.hideItem("updateAll");
this.hideItem("copyFeedURL");
}
else {
// menu when single feed selected
this.showItem("update");
this.showItem("markRead");
this.showItem("rename");
this.showItem("delete");
this.showItem("newSubscription");
this.hideItem("newFolder");
this.hideItem("updateAll");
this.showItem("copyFeedURL");
}
break;
default:
// menu when multiple items selected
this.showItem("update");
this.showItem("markRead");
this.hideItem("rename");
this.showItem("delete");
this.hideItem("newSubscription");
this.hideItem("newFolder");
this.hideItem("updateAll");
this.showItem("copyFeedURL");
break;
}
}
});
const RssArticleContextMenu = new Class({
Extends: ContextMenu
});
const RssDownloaderRuleContextMenu = new Class({
Extends: ContextMenu,
adjustMenuPosition: function(e) {
this.updateMenuItems();
// draw the menu off-screen to know the menu dimensions
this.menu.setStyles({
left: "-999em",
top: "-999em"
});
// position the menu
let xPosMenu = e.page.x + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
let yPosMenu = e.page.y + this.options.offsets.y - $("rssdownloaderpage").offsetTop;
if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
xPosMenu -= this.menu.offsetWidth;
if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
xPosMenu = Math.max(xPosMenu, 0);
yPosMenu = Math.max(yPosMenu, 0);
this.menu.setStyles({
left: xPosMenu,
top: yPosMenu,
position: "absolute",
"z-index": "2000"
});
},
updateMenuItems: function() {
let selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
this.showItem("addRule");
switch (selectedRows.length) {
case 0:
// menu when nothing selected
this.hideItem("deleteRule");
this.hideItem("renameRule");
this.hideItem("clearDownloadedEpisodes");
break;
case 1:
// menu when single item selected
this.showItem("deleteRule");
this.showItem("renameRule");
this.showItem("clearDownloadedEpisodes");
break;
default:
// menu when multiple items selected
this.showItem("deleteRule");
this.hideItem("renameRule");
this.showItem("clearDownloadedEpisodes");
break;
}
}
});
return exports();
})();
Object.freeze(window.qBittorrent.ContextMenu);