WebUI: Provide 'Merge trackers to existing torrent' option
[qBittorrent.git] / src / webui / www / private / scripts / misc.js
blob4fff5d4feee957db8ab981d6d9fe6151be6c8a1e
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2014 Gabriele <pmzqla.git@gmail.com>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 "use strict";
31 window.qBittorrent ??= {};
32 window.qBittorrent.Misc ??= (() => {
33 const exports = () => {
34 return {
35 genHash: genHash,
36 getHost: getHost,
37 createDebounceHandler: createDebounceHandler,
38 friendlyUnit: friendlyUnit,
39 friendlyDuration: friendlyDuration,
40 friendlyPercentage: friendlyPercentage,
41 friendlyFloat: friendlyFloat,
42 parseHtmlLinks: parseHtmlLinks,
43 parseVersion: parseVersion,
44 escapeHtml: escapeHtml,
45 naturalSortCollator: naturalSortCollator,
46 safeTrim: safeTrim,
47 toFixedPointString: toFixedPointString,
48 containsAllTerms: containsAllTerms,
49 sleep: sleep,
50 // variables
51 FILTER_INPUT_DELAY: 400,
52 MAX_ETA: 8640000
56 const genHash = function(string) {
57 // origins:
58 // https://stackoverflow.com/a/8831937
59 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
60 let hash = 0;
61 for (let i = 0; i < string.length; ++i)
62 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
63 return hash;
66 // getHost emulate the GUI version `QString getHost(const QString &url)`
67 const getHost = function(url) {
68 // We want the hostname.
69 // If failed to parse the domain, original input should be returned
71 if (!/^(?:https?|udp):/i.test(url))
72 return url;
74 try {
75 // hack: URL can not get hostname from udp protocol
76 const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
77 // host: "example.com:8443"
78 // hostname: "example.com"
79 const host = parsedUrl.hostname;
80 if (!host)
81 return url;
83 return host;
85 catch (error) {
86 return url;
90 const createDebounceHandler = (delay, func) => {
91 let timer = -1;
92 return (...params) => {
93 clearTimeout(timer);
94 timer = setTimeout(() => {
95 func(...params);
97 timer = -1;
98 }, delay);
103 * JS counterpart of the function in src/misc.cpp
105 const friendlyUnit = function(value, isSpeed) {
106 const units = [
107 "QBT_TR(B)QBT_TR[CONTEXT=misc]",
108 "QBT_TR(KiB)QBT_TR[CONTEXT=misc]",
109 "QBT_TR(MiB)QBT_TR[CONTEXT=misc]",
110 "QBT_TR(GiB)QBT_TR[CONTEXT=misc]",
111 "QBT_TR(TiB)QBT_TR[CONTEXT=misc]",
112 "QBT_TR(PiB)QBT_TR[CONTEXT=misc]",
113 "QBT_TR(EiB)QBT_TR[CONTEXT=misc]"
116 if ((value === undefined) || (value === null) || (value < 0))
117 return "QBT_TR(Unknown)QBT_TR[CONTEXT=misc]";
119 let i = 0;
120 while ((value >= 1024.0) && (i < 6)) {
121 value /= 1024.0;
122 ++i;
125 function friendlyUnitPrecision(sizeUnit) {
126 if (sizeUnit <= 2) // KiB, MiB
127 return 1;
128 else if (sizeUnit === 3) // GiB
129 return 2;
130 else // TiB, PiB, EiB
131 return 3;
134 let ret;
135 if (i === 0) {
136 ret = value + " " + units[i];
138 else {
139 const precision = friendlyUnitPrecision(i);
140 const offset = Math.pow(10, precision);
141 // Don't round up
142 ret = (Math.floor(offset * value) / offset).toFixed(precision) + " " + units[i];
145 if (isSpeed)
146 ret += "QBT_TR(/s)QBT_TR[CONTEXT=misc]";
147 return ret;
151 * JS counterpart of the function in src/misc.cpp
153 const friendlyDuration = function(seconds, maxCap = -1) {
154 if ((seconds < 0) || ((seconds >= maxCap) && (maxCap >= 0)))
155 return "∞";
156 if (seconds === 0)
157 return "0";
158 if (seconds < 60)
159 return "QBT_TR(< 1m)QBT_TR[CONTEXT=misc]";
160 let minutes = seconds / 60;
161 if (minutes < 60)
162 return "QBT_TR(%1m)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(minutes));
163 let hours = minutes / 60;
164 minutes %= 60;
165 if (hours < 24)
166 return "QBT_TR(%1h %2m)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(hours)).replace("%2", Math.floor(minutes));
167 let days = hours / 24;
168 hours %= 24;
169 if (days < 365)
170 return "QBT_TR(%1d %2h)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(days)).replace("%2", Math.floor(hours));
171 const years = days / 365;
172 days %= 365;
173 return "QBT_TR(%1y %2d)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(years)).replace("%2", Math.floor(days));
176 const friendlyPercentage = function(value) {
177 let percentage = (value * 100).round(1);
178 if (isNaN(percentage) || (percentage < 0))
179 percentage = 0;
180 if (percentage > 100)
181 percentage = 100;
182 return percentage.toFixed(1) + "%";
185 const friendlyFloat = function(value, precision) {
186 return parseFloat(value).toFixed(precision);
190 * JS counterpart of the function in src/misc.cpp
192 const parseHtmlLinks = function(text) {
193 const exp = /(\b(https?|ftp|file):\/\/[-\w+&@#/%?=~|!:,.;]*[-\w+&@#/%=~|])/gi;
194 return text.replace(exp, "<a target='_blank' rel='noopener noreferrer' href='$1'>$1</a>");
197 const parseVersion = function(versionString) {
198 const failure = {
199 valid: false
202 if (typeof versionString !== "string")
203 return failure;
205 const tryToNumber = (str) => {
206 const num = Number(str);
207 return (isNaN(num) ? str : num);
210 const ver = versionString.split(".", 4).map(val => tryToNumber(val));
211 return {
212 valid: true,
213 major: ver[0],
214 minor: ver[1],
215 fix: ver[2],
216 patch: ver[3]
220 const escapeHtml = (() => {
221 const div = document.createElement("div");
222 return (str) => {
223 div.textContent = str;
224 return div.innerHTML;
226 })();
228 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#parameters
229 const naturalSortCollator = new Intl.Collator(undefined, { numeric: true, usage: "sort" });
231 const safeTrim = function(value) {
232 try {
233 return value.trim();
235 catch (e) {
236 if (e instanceof TypeError)
237 return "";
238 throw e;
242 const toFixedPointString = function(number, digits) {
243 // Do not round up number
244 const power = Math.pow(10, digits);
245 return (Math.floor(power * number) / power).toFixed(digits);
250 * @param {String} text the text to search
251 * @param {Array<String>} terms terms to search for within the text
252 * @returns {Boolean} true if all terms match the text, false otherwise
254 const containsAllTerms = function(text, terms) {
255 const textToSearch = text.toLowerCase();
256 return terms.every((term) => {
257 const isTermRequired = (term[0] === "+");
258 const isTermExcluded = (term[0] === "-");
259 if (isTermRequired || isTermExcluded) {
260 // ignore lonely +/-
261 if (term.length === 1)
262 return true;
264 term = term.substring(1);
267 const textContainsTerm = (textToSearch.indexOf(term) !== -1);
268 return isTermExcluded ? !textContainsTerm : textContainsTerm;
272 const sleep = (ms) => {
273 return new Promise((resolve) => {
274 setTimeout(resolve, ms);
278 return exports();
279 })();
280 Object.freeze(window.qBittorrent.Misc);