/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2009 Christophe Dumez * * 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: 'QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget] QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]' })); contextCategoryList.appendChild(new Element("li", { html: 'QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget] QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]' })); 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: `${window.qBittorrent.Misc.escapeHtml(categoryName)}` }); 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: '' + 'QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]' + " QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]" + "" })); contextTagList.appendChild(new Element("li", { html: '' + 'QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]' + " QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]" + "" })); 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: `` + ' ' + window.qBittorrent.Misc.escapeHtml(tagName) + "" }); 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);