diff --git a/.github/workflows/ci_macos.yaml b/.github/workflows/ci_macos.yaml index 53e0def991c4..d1f7cf77c75e 100644 --- a/.github/workflows/ci_macos.yaml +++ b/.github/workflows/ci_macos.yaml @@ -32,6 +32,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Install dependencies uses: Wandalen/wretry.action@v3 env: @@ -103,6 +108,11 @@ jobs: cmake --build build sudo cmake --install build + - name: Install node dependencies + working-directory: src/webui/www + run: | + npm install + - name: Build qBittorrent run: | CXXFLAGS="$CXXFLAGS -DQT_FORCE_ASSERTS -Werror -Wno-error=deprecated-declarations" \ diff --git a/.github/workflows/ci_ubuntu.yaml b/.github/workflows/ci_ubuntu.yaml index 83852fd61bf5..120222359fec 100644 --- a/.github/workflows/ci_ubuntu.yaml +++ b/.github/workflows/ci_ubuntu.yaml @@ -34,6 +34,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Install dependencies run: | sudo apt update @@ -98,6 +103,11 @@ jobs: cmake --build build sudo cmake --install build + - name: Install node dependencies + working-directory: src/webui/www + run: | + npm install + # to avoid scanning 3rdparty codebases, initialize it just before building qbt - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/ci_webui.yaml b/.github/workflows/ci_webui.yaml index 43228af2b25c..9b49535d16a8 100644 --- a/.github/workflows/ci_webui.yaml +++ b/.github/workflows/ci_webui.yaml @@ -38,6 +38,9 @@ jobs: npm ls --all echo "::endgroup::" + - name: Compile TS + run: npm run build + - name: Run tests run: npm test diff --git a/.github/workflows/ci_windows.yaml b/.github/workflows/ci_windows.yaml index 446bee5fc1df..df22d29a8016 100644 --- a/.github/workflows/ci_windows.yaml +++ b/.github/workflows/ci_windows.yaml @@ -31,6 +31,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Setup devcmd uses: ilammy/msvc-dev-cmd@v1 @@ -138,6 +143,11 @@ jobs: cmake --build build cmake --install build + - name: Install node dependencies + working-directory: src/webui/www + run: | + npm install + - name: Build qBittorrent run: | $env:CXXFLAGS+="/DQT_FORCE_ASSERTS /WX" diff --git a/.github/workflows/coverity-scan.yaml b/.github/workflows/coverity-scan.yaml index 76c9fc8ab41f..584f5cd0e705 100644 --- a/.github/workflows/coverity-scan.yaml +++ b/.github/workflows/coverity-scan.yaml @@ -29,6 +29,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Install dependencies run: | sudo apt update @@ -82,6 +87,11 @@ jobs: cmake --build build sudo cmake --install build + - name: Install node dependencies + working-directory: src/webui/www + run: | + npm install + - name: Download Coverity Build Tool run: | curl \ diff --git a/src/webui/CMakeLists.txt b/src/webui/CMakeLists.txt index 377a8356ba2e..f4b6e9c856b5 100644 --- a/src/webui/CMakeLists.txt +++ b/src/webui/CMakeLists.txt @@ -34,6 +34,48 @@ add_library(qbt_webui STATIC webui.cpp ) +# Build TypeScript files before processing webui.qrc +find_program(NPM_EXECUTABLE npm) +if(NOT NPM_EXECUTABLE) + message(FATAL_ERROR "npm is required to build the WebUI. Please install Node.js and npm.") +endif() + +message(STATUS "Found npm: ${NPM_EXECUTABLE}") + +# Find all TypeScript source files +file(GLOB_RECURSE WEBUI_TS_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/www/private/scripts/*.ts" +) +# Exclude .d.ts files from dependencies +list(FILTER WEBUI_TS_FILES EXCLUDE REGEX ".*\\.d\\.ts$") + +# Generate list of output JS files +set(WEBUI_JS_FILES) +foreach(ts_file ${WEBUI_TS_FILES}) + string(REPLACE ".ts" ".js" js_file ${ts_file}) + list(APPEND WEBUI_JS_FILES ${js_file}) +endforeach() + +# Custom command to build TypeScript +add_custom_command( + OUTPUT ${WEBUI_JS_FILES} + COMMAND ${NPM_EXECUTABLE} run build + DEPENDS ${WEBUI_TS_FILES} + ${CMAKE_CURRENT_SOURCE_DIR}/www/tsconfig.json + ${CMAKE_CURRENT_SOURCE_DIR}/www/package.json + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/www + COMMENT "Compiling TypeScript files to JavaScript" + VERBATIM +) + +# Create a custom target for TypeScript compilation +add_custom_target(qbt_webui_typescript + DEPENDS ${WEBUI_JS_FILES} +) + +# Make sure TypeScript is built before the webui library +add_dependencies(qbt_webui qbt_webui_typescript) + target_sources(qbt_webui INTERFACE www/webui.qrc) target_link_libraries(qbt_webui PRIVATE qbt_base) diff --git a/src/webui/www/.gitignore b/src/webui/www/.gitignore index 2f85f13cb7f0..a81817fedf93 100644 --- a/src/webui/www/.gitignore +++ b/src/webui/www/.gitignore @@ -3,3 +3,6 @@ .stylelintcache node_modules package-lock.json + +# ts built files +private/scripts/*.js diff --git a/src/webui/www/dprint.json b/src/webui/www/dprint.json new file mode 100644 index 000000000000..13222c8967ee --- /dev/null +++ b/src/webui/www/dprint.json @@ -0,0 +1,36 @@ +{ + "indentWidth": 4, + "useTabs": false, + "newLineKind": "lf", + "lineWidth": 1000, + "typescript": { + "bracePosition": "sameLine", + "singleBodyPosition": "nextLine", + "nextControlFlowPosition": "maintain", + "useBraces": "maintain", + "operatorPosition": "maintain", + "binaryExpression.operatorPosition": "nextLine", + "memberExpression.linePerExpression": false, + "binaryExpression.linePerExpression": false, + "trailingCommas": "onlyMultiLine", + "quoteStyle": "preferDouble", + "quoteProps": "asNeeded", + "semiColons": "always", + "arguments.preferSingleLine": true, + "conditionalExpression.preferSingleLine": true, + "parameters.preferSingleLine": true, + "parentheses.preferSingleLine": true, + "preferHanging": false, + "arguments.preferHanging": "never", + "arguments.trailingCommas": "never", + "forOfStatement.preferHanging": false, + "forStatement.preferHanging": false, + "ifStatement.preferHanging": false + }, + "excludes": [ + "**/node_modules" + ], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.95.11.wasm" + ] +} diff --git a/src/webui/www/eslint.config.mjs b/src/webui/www/eslint.config.mjs index 5d78ea107dfd..5665840eb49c 100644 --- a/src/webui/www/eslint.config.mjs +++ b/src/webui/www/eslint.config.mjs @@ -4,6 +4,8 @@ import Js from "@eslint/js"; import PluginQbtWebUI from "eslint-plugin-qbt-webui"; import PreferArrowFunctions from "eslint-plugin-prefer-arrow-functions"; import Stylistic from "@stylistic/eslint-plugin"; +import TypescriptEslint from "@typescript-eslint/eslint-plugin"; +import TypescriptParser from "@typescript-eslint/parser"; import Unicorn from "eslint-plugin-unicorn"; import * as RegexpPlugin from "eslint-plugin-regexp"; @@ -16,10 +18,11 @@ export default [ files: [ "**/*.html", "**/*.js", - "**/*.mjs" + "**/*.mjs", + "**/*.ts" ], languageOptions: { - ecmaVersion: 2022, + ecmaVersion: 2023, globals: { ...Globals.browser } @@ -76,5 +79,18 @@ export default [ "Unicorn/prefer-number-properties": "error", "Unicorn/switch-case-braces": ["error", "avoid"] } + }, + // TypeScript files - use same rules but with TypeScript parser + { + files: ["**/*.ts"], + languageOptions: { + parser: TypescriptParser, + parserOptions: { + project: "./tsconfig.json" + } + }, + plugins: { + "@typescript-eslint": TypescriptEslint + } } ]; diff --git a/src/webui/www/package.json b/src/webui/www/package.json index 4e301b0a8663..800dd92c05aa 100644 --- a/src/webui/www/package.json +++ b/src/webui/www/package.json @@ -6,12 +6,17 @@ "url": "https://github.com/qbittorrent/qBittorrent.git" }, "scripts": { - "format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js test/*/*.js && prettier --write **.css", - "lint": "eslint --cache *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js test/*/*.js && stylelint --cache **/*.css && html-validate private public", + "build": "tsc", + "build:watch": "tsc --watch", + "format": "js-beautify -r *.mjs private/*.html private/views/*.html public/*.html public/scripts/*.js test/*/*.js && dprint fmt private/scripts/*.ts && prettier --write **.css", + "lint": "eslint --cache *.mjs private/*.html private/scripts/*.ts private/views/*.html public/*.html public/scripts/*.js test/*/*.js && stylelint --cache **/*.css && html-validate private public", "test": "vitest run --dom" }, "devDependencies": { "@stylistic/eslint-plugin": "*", + "@typescript-eslint/eslint-plugin": "*", + "@typescript-eslint/parser": "*", + "dprint": "^0.50.1", "eslint": "*", "eslint-plugin-html": "*", "eslint-plugin-prefer-arrow-functions": "*", @@ -25,6 +30,7 @@ "stylelint": "*", "stylelint-config-standard": "*", "stylelint-order": "*", + "typescript": "^5.0.0", "vitest": "*" } } diff --git a/src/webui/www/private/scripts/addtorrent.js b/src/webui/www/private/scripts/addtorrent.ts similarity index 74% rename from src/webui/www/private/scripts/addtorrent.js rename to src/webui/www/private/scripts/addtorrent.ts index 4990eff28107..cc68cd327590 100644 --- a/src/webui/www/private/scripts/addtorrent.js +++ b/src/webui/www/private/scripts/addtorrent.ts @@ -21,10 +21,8 @@ * THE SOFTWARE. */ -"use strict"; - window.qBittorrent ??= {}; -window.qBittorrent.AddTorrent ??= (() => { +const addTorrentModule = (() => { const exports = () => { return { changeCategorySelect: changeCategorySelect, @@ -33,7 +31,7 @@ window.qBittorrent.AddTorrent ??= (() => { metadataCompleted: metadataCompleted, populateMetadata: populateMetadata, setWindowId: setWindowId, - submitForm: submitForm + submitForm: submitForm, }; }; @@ -77,7 +75,7 @@ window.qBittorrent.AddTorrent ??= (() => { translations: { all: (window.parent.qBittorrent.Client.tagMap.length === 0) ? "" : "QBT_TR(All)QBT_TR[CONTEXT=AddNewTorrentDialog]", }, - keepInlineStyles: false + keepInlineStyles: false, }); }; @@ -87,32 +85,34 @@ window.qBittorrent.AddTorrent ??= (() => { defaultSavePath = pref.save_path; defaultTempPath = pref.temp_path; defaultTempPathEnabled = pref.temp_path_enabled; - document.getElementById("startTorrent").checked = !pref.add_stopped_enabled; - document.getElementById("addToTopOfQueue").checked = pref.add_to_top_of_queue; + window.qBittorrent.Misc.getElementById("startTorrent", "input").checked = !pref.add_stopped_enabled; + window.qBittorrent.Misc.getElementById("addToTopOfQueue", "input").checked = pref.add_to_top_of_queue; - const autoTMM = document.getElementById("autoTMM"); + const autoTMM = window.qBittorrent.Misc.getElementById("autoTMM", "select"); if (pref.auto_tmm_enabled) { autoTMM.selectedIndex = 1; - document.getElementById("savepath").disabled = true; + window.qBittorrent.Misc.getElementById("savepath", "input").disabled = true; } else { autoTMM.selectedIndex = 0; } changeTMM(); + const stopConditionSelect = window.qBittorrent.Misc.getElementById("stopCondition", "select"); if (pref.torrent_stop_condition === "MetadataReceived") - document.getElementById("stopCondition").selectedIndex = 1; + stopConditionSelect.selectedIndex = 1; else if (pref.torrent_stop_condition === "FilesChecked") - document.getElementById("stopCondition").selectedIndex = 2; + stopConditionSelect.selectedIndex = 2; else - document.getElementById("stopCondition").selectedIndex = 0; + stopConditionSelect.selectedIndex = 0; + const contentLayoutSelect = window.qBittorrent.Misc.getElementById("contentLayout", "select"); if (pref.torrent_content_layout === "Subfolder") - document.getElementById("contentLayout").selectedIndex = 1; + contentLayoutSelect.selectedIndex = 1; else if (pref.torrent_content_layout === "NoSubfolder") - document.getElementById("contentLayout").selectedIndex = 2; + contentLayoutSelect.selectedIndex = 2; else - document.getElementById("contentLayout").selectedIndex = 0; + contentLayoutSelect.selectedIndex = 0; }; const categorySavePath = (categoryName) => { @@ -144,10 +144,10 @@ window.qBittorrent.AddTorrent ??= (() => { item.nextElementSibling.select(); if (isAutoTMMEnabled()) { - document.getElementById("savepath").value = defaultSavePath; + window.qBittorrent.Misc.getElementById("savepath", "input").value = defaultSavePath; const downloadPathEnabled = categoryDownloadPathEnabled(categoryName); - document.getElementById("useDownloadPath").checked = downloadPathEnabled; + window.qBittorrent.Misc.getElementById("useDownloadPath", "input").checked = downloadPathEnabled; changeUseDownloadPath(downloadPathEnabled); } } @@ -157,10 +157,10 @@ window.qBittorrent.AddTorrent ??= (() => { item.nextElementSibling.value = text; if (isAutoTMMEnabled()) { - document.getElementById("savepath").value = categorySavePath(categoryName); + window.qBittorrent.Misc.getElementById("savepath", "input").value = categorySavePath(categoryName); const downloadPathEnabled = categoryDownloadPathEnabled(categoryName); - document.getElementById("useDownloadPath").checked = downloadPathEnabled; + window.qBittorrent.Misc.getElementById("useDownloadPath", "input").checked = downloadPathEnabled; changeUseDownloadPath(downloadPathEnabled); } } @@ -168,20 +168,20 @@ window.qBittorrent.AddTorrent ??= (() => { const changeTagsSelect = (element) => { const tags = [...element.options].filter(opt => opt.selected).map(opt => opt.value); - document.getElementById("tags").value = tags.join(","); + window.qBittorrent.Misc.getElementById("tags", "input").value = tags.join(","); }; const isAutoTMMEnabled = () => { - return document.getElementById("autoTMM").selectedIndex === 1; + return window.qBittorrent.Misc.getElementById("autoTMM", "select").selectedIndex === 1; }; const changeTMM = () => { const autoTMMEnabled = isAutoTMMEnabled(); - const savepath = document.getElementById("savepath"); - const useDownloadPath = document.getElementById("useDownloadPath"); + const savepath = window.qBittorrent.Misc.getElementById("savepath", "input"); + const useDownloadPath = window.qBittorrent.Misc.getElementById("useDownloadPath", "input"); if (autoTMMEnabled) { - const categorySelect = document.getElementById("categorySelect"); + const categorySelect = window.qBittorrent.Misc.getElementById("categorySelect", "select"); const categoryName = categorySelect.options[categorySelect.selectedIndex].value; savepath.value = categorySavePath(categoryName); useDownloadPath.checked = categoryDownloadPathEnabled(categoryName); @@ -195,15 +195,15 @@ window.qBittorrent.AddTorrent ??= (() => { useDownloadPath.disabled = autoTMMEnabled; // only submit this value when using manual tmm - document.getElementById("useDownloadPathHidden").disabled = autoTMMEnabled; + window.qBittorrent.Misc.getElementById("useDownloadPathHidden", "input").disabled = autoTMMEnabled; changeUseDownloadPath(useDownloadPath.checked); }; const changeUseDownloadPath = (enabled) => { - const downloadPath = document.getElementById("downloadPath"); + const downloadPath = window.qBittorrent.Misc.getElementById("downloadPath", "input"); if (isAutoTMMEnabled()) { - const categorySelect = document.getElementById("categorySelect"); + const categorySelect = window.qBittorrent.Misc.getElementById("categorySelect", "select"); const categoryName = categorySelect.options[categorySelect.selectedIndex].value; downloadPath.value = enabled ? categoryDownloadPath(categoryName) : ""; downloadPath.disabled = true; @@ -222,12 +222,12 @@ window.qBittorrent.AddTorrent ??= (() => { downloader = downloaderName; fetch("api/v2/torrents/fetchMetadata", { - method: "POST", - body: new URLSearchParams({ - source: source, - downloader: downloader - }) - }) + method: "POST", + body: new URLSearchParams({ + source: source, + downloader: downloader, + }), + }) .then(async (response) => { if (!response.ok) { metadataFailed(); @@ -240,11 +240,11 @@ window.qBittorrent.AddTorrent ??= (() => { if (response.status === 200) metadataCompleted(); else - loadMetadataTimer = loadMetadata.delay(1000); + loadMetadataTimer = window.setTimeout(loadMetadata, 1000); }, (error) => { console.error(error); - loadMetadataTimer = loadMetadata.delay(1000); + loadMetadataTimer = window.setTimeout(loadMetadata, 1000); }); }; @@ -252,7 +252,7 @@ window.qBittorrent.AddTorrent ??= (() => { clearTimeout(loadMetadataTimer); loadMetadataTimer = -1; - document.getElementById("metadataStatus").destroy(); + document.getElementById("metadataStatus").remove(); document.getElementById("loadingSpinner").style.display = "none"; if (showDownloadButton) @@ -302,23 +302,24 @@ window.qBittorrent.AddTorrent ??= (() => { }; const submitForm = () => { - document.getElementById("startTorrentHidden").value = document.getElementById("startTorrent").checked ? "false" : "true"; + window.qBittorrent.Misc.getElementById("startTorrentHidden", "input").value = window.qBittorrent.Misc.getElementById("startTorrent", "input").checked ? "false" : "true"; - document.getElementById("dlLimitHidden").value = Number(document.getElementById("dlLimitText").value) * 1024; - document.getElementById("upLimitHidden").value = Number(document.getElementById("upLimitText").value) * 1024; + window.qBittorrent.Misc.getElementById("dlLimitHidden", "input").value = String(Number(window.qBittorrent.Misc.getElementById("dlLimitText", "input").value) * 1024); + window.qBittorrent.Misc.getElementById("upLimitHidden", "input").value = String(Number(window.qBittorrent.Misc.getElementById("upLimitText", "input").value) * 1024); - document.getElementById("filePriorities").value = [...document.getElementsByClassName("combo_priority")] + window.qBittorrent.Misc.getElementById("filePriorities", "input").value = [...document.getElementsByClassName("combo_priority") as unknown as HTMLInputElement[]] .filter((el) => !window.qBittorrent.TorrentContent.isFolder(Number(el.dataset.fileId))) .sort((el1, el2) => Number(el1.dataset.fileId) - Number(el2.dataset.fileId)) - .map((el) => Number(el.value)); + .map((el) => (el.value)) + .toString(); if (!isAutoTMMEnabled()) - document.getElementById("useDownloadPathHidden").value = document.getElementById("useDownloadPath").checked; + window.qBittorrent.Misc.getElementById("useDownloadPathHidden", "input").value = String(window.qBittorrent.Misc.getElementById("useDownloadPath", "input").checked); document.getElementById("loadingSpinner").style.display = "block"; - if (document.getElementById("setDefaultCategory").checked) { - const category = document.getElementById("category").value.trim(); + if (window.qBittorrent.Misc.getElementById("setDefaultCategory", "input").checked) { + const category = window.qBittorrent.Misc.getElementById("category", "input").value.trim(); if (category.length === 0) localPreferences.remove("add_torrent_default_category"); else @@ -337,10 +338,12 @@ window.qBittorrent.AddTorrent ??= (() => { }); window.addEventListener("DOMContentLoaded", (event) => { - document.getElementById("useDownloadPath").addEventListener("change", (e) => changeUseDownloadPath(e.target.checked)); + document.getElementById("useDownloadPath").addEventListener("change", (e) => changeUseDownloadPath((e.target as HTMLInputElement).checked)); document.getElementById("tagsSelect").addEventListener("change", (e) => changeTagsSelect(e.target)); }); return exports(); })(); + +window.qBittorrent.AddTorrent ??= addTorrentModule; Object.freeze(window.qBittorrent.AddTorrent); diff --git a/src/webui/www/private/scripts/cache.js b/src/webui/www/private/scripts/cache.ts similarity index 87% rename from src/webui/www/private/scripts/cache.js rename to src/webui/www/private/scripts/cache.ts index 25abb4c33400..919daa62200f 100644 --- a/src/webui/www/private/scripts/cache.js +++ b/src/webui/www/private/scripts/cache.ts @@ -26,15 +26,13 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; -window.qBittorrent.Cache ??= (() => { +const cacheModule = (() => { const exports = () => { return { buildInfo: new BuildInfoCache(), preferences: new PreferencesCache(), - qbtVersion: new QbtVersionCache() + qbtVersion: new QbtVersionCache(), }; }; @@ -51,13 +49,13 @@ window.qBittorrent.Cache ??= (() => { }; class BuildInfoCache { - #m_store = {}; + #m_store: Record = {}; init() { return fetch("api/v2/app/buildInfo", { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -74,13 +72,13 @@ window.qBittorrent.Cache ??= (() => { } class PreferencesCache { - #m_store = {}; + #m_store: Record = {}; async init() { return await fetch("api/v2/app/preferences", { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -103,11 +101,11 @@ window.qBittorrent.Cache ??= (() => { throw new Error("`data` is not an object."); return await fetch("api/v2/app/setPreferences", { - method: "POST", - body: new URLSearchParams({ - json: JSON.stringify(data) - }) - }) + method: "POST", + body: new URLSearchParams({ + json: JSON.stringify(data), + }), + }) .then((response) => { if (!response.ok) return; @@ -130,9 +128,9 @@ window.qBittorrent.Cache ??= (() => { init() { return fetch("api/v2/app/version", { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -149,4 +147,6 @@ window.qBittorrent.Cache ??= (() => { return exports(); })(); + +window.qBittorrent.Cache ??= cacheModule; Object.freeze(window.qBittorrent.Cache); diff --git a/src/webui/www/private/scripts/client.js b/src/webui/www/private/scripts/client.ts similarity index 77% rename from src/webui/www/private/scripts/client.js rename to src/webui/www/private/scripts/client.ts index 6e31441116e5..f3914f86884c 100644 --- a/src/webui/www/private/scripts/client.js +++ b/src/webui/www/private/scripts/client.ts @@ -23,7 +23,7 @@ * THE SOFTWARE. */ -"use strict"; +type Category = { categoryName: string; categoryCount: number; nameSegments: string[]; } & { children?: any[]; isRoot?: boolean; forceExpand?: boolean; }; window.qBittorrent ??= {}; window.qBittorrent.Client ??= (() => { @@ -46,14 +46,14 @@ window.qBittorrent.Client ??= (() => { createAddTorrentWindow: createAddTorrentWindow, uploadTorrentFiles: uploadTorrentFiles, categoryMap: categoryMap, - tagMap: tagMap + tagMap: tagMap, }; }; // Map - const categoryMap = new Map(); + const categoryMap: Map; }> = new Map(); // Map - const tagMap = new Map(); + const tagMap: Map> = new Map(); let cacheAllSettled; const setup = () => { @@ -61,7 +61,7 @@ window.qBittorrent.Client ??= (() => { cacheAllSettled = Promise.allSettled([ window.qBittorrent.Cache.buildInfo.init(), window.qBittorrent.Cache.preferences.init(), - window.qBittorrent.Cache.qbtVersion.init() + window.qBittorrent.Cache.qbtVersion.init(), ]); }; @@ -142,14 +142,14 @@ window.qBittorrent.Client ??= (() => { const staticId = "uploadPage"; const id = `${staticId}-${encodeURIComponent(source)}`; - const contentURL = new URL("addtorrent.html", window.location); + const contentURL = new URL("addtorrent.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", source: source, - fetch: metadata === undefined, + fetch: String(metadata === undefined), windowId: id, - downloader: downloader ?? "" - }); + downloader: downloader ?? "", + }).toString(); new MochaUI.Window({ id: id, @@ -168,12 +168,12 @@ window.qBittorrent.Client ??= (() => { }), onContentLoaded: () => { if (metadata !== undefined) - document.getElementById(`${id}_iframe`).contentWindow.postMessage(metadata, window.origin); - } + window.qBittorrent.Misc.getElementById(`${id}_iframe`, "frame").contentWindow.postMessage(metadata, window.origin); + }, }); }; - const uploadTorrentFiles = (files) => { + const uploadTorrentFiles = (files: FileList) => { const fileNames = []; const formData = new FormData(); for (const [i, file] of Array.prototype.entries.call(files)) { @@ -183,9 +183,9 @@ window.qBittorrent.Client ??= (() => { } fetch("api/v2/torrents/parseMetadata", { - method: "POST", - body: formData - }) + method: "POST", + body: formData, + }) .then(async (response) => { if (!response.ok) { alert(await response.text()); @@ -231,14 +231,14 @@ const CATEGORIES_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655"; const CATEGORIES_UNCATEGORIZED = "e24bd469-ea22-404c-8e2e-a17c82f37ea0"; let selectedCategory = localPreferences.get("selected_category", CATEGORIES_ALL); -let setCategoryFilter = () => {}; +let setCategoryFilter = (category) => {}; /* Tags filter */ const TAGS_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655"; const TAGS_UNTAGGED = "e24bd469-ea22-404c-8e2e-a17c82f37ea0"; let selectedTag = localPreferences.get("selected_tag", TAGS_ALL); -let setTagFilter = () => {}; +let setTagFilter = (tag) => {}; /* Trackers filter */ const TRACKERS_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655"; @@ -247,16 +247,15 @@ const TRACKERS_ERROR = "b551cfc3-64e9-4393-bc88-5d6ea2fab5cc"; const TRACKERS_TRACKERLESS = "e24bd469-ea22-404c-8e2e-a17c82f37ea0"; const TRACKERS_WARNING = "82a702c5-210c-412b-829f-97632d7557e9"; -// Map> -const trackerMap = new Map(); +const trackerMap: Map>> = new Map(); let selectedTracker = localPreferences.get("selected_tracker", TRACKERS_ALL); -let setTrackerFilter = () => {}; +let setTrackerFilter = (tracker) => {}; /* All filters */ let selectedStatus = localPreferences.get("selected_filter", "all"); -let setStatusFilter = () => {}; -let toggleFilterDisplay = () => {}; +let setStatusFilter = (name) => {}; +let toggleFilterDisplay = (filterListID) => {}; window.addEventListener("DOMContentLoaded", (event) => { window.qBittorrent.LocalPreferences.upgrade(); @@ -268,15 +267,18 @@ window.addEventListener("DOMContentLoaded", (event) => { const saveColumnSizes = () => { const filters_width = document.getElementById("Filters").getSize().x; localPreferences.set("filters_width", filters_width); - const properties_height_rel = document.getElementById("propertiesPanel").getSize().y / Window.getSize().y; + const properties_height_rel = document.getElementById("propertiesPanel").getSize().y / document.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 (!document.getElementById("mainColumn").classList.contains("invisible")) - saveColumnSizes(); - })); + window.addEventListener( + "resize", + window.qBittorrent.Misc.createDebounceHandler(500, (e) => { + // only save sizes if the columns are visible + if (!document.getElementById("mainColumn").classList.contains("invisible")) + saveColumnSizes(); + }) + ); /* MochaUI.Desktop = new MochaUI.Desktop(); MochaUI.Desktop.desktop.style.background = "#fff"; @@ -291,11 +293,11 @@ window.addEventListener("DOMContentLoaded", (event) => { saveColumnSizes(); }), width: Number(localPreferences.get("filters_width", 210)), - resizeLimit: [1, 1000] + resizeLimit: [1, 1000], }); new MochaUI.Column({ id: "mainColumn", - placement: "main" + placement: "main", }); }; @@ -303,7 +305,7 @@ window.addEventListener("DOMContentLoaded", (event) => { new MochaUI.Column({ id: "searchTabColumn", placement: "main", - width: null + width: null, }); // start off hidden @@ -314,7 +316,7 @@ window.addEventListener("DOMContentLoaded", (event) => { new MochaUI.Column({ id: "rssTabColumn", placement: "main", - width: null + width: null, }); // start off hidden @@ -325,7 +327,7 @@ window.addEventListener("DOMContentLoaded", (event) => { new MochaUI.Column({ id: "logTabColumn", placement: "main", - width: null + width: null, }); // start off hidden @@ -413,7 +415,7 @@ window.addEventListener("DOMContentLoaded", (event) => { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, loadMethod: "xhr", contentURL: "views/filters.html?v=${CACHEID}", @@ -421,35 +423,35 @@ window.addEventListener("DOMContentLoaded", (event) => { highlightSelectedStatus(); }, column: "filtersColumn", - height: 300 + height: 300, }); initializeWindows(); // Show Top Toolbar is enabled by default let showTopToolbar = localPreferences.get("show_top_toolbar", "true") === "true"; if (!showTopToolbar) { - document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showTopToolbarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("mochaToolbar").classList.add("invisible"); } // Show Status Bar is enabled by default let showStatusBar = localPreferences.get("show_status_bar", "true") === "true"; if (!showStatusBar) { - document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showStatusBarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("desktopFooterWrapper").classList.add("invisible"); } // Show Filters Sidebar is enabled by default let showFiltersSidebar = localPreferences.get("show_filters_sidebar", "true") === "true"; if (!showFiltersSidebar) { - document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showFiltersSidebarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("filtersColumn").classList.add("invisible"); document.getElementById("filtersColumn_handle").classList.add("invisible"); } let speedInTitle = localPreferences.get("speed_in_browser_title_bar") === "true"; if (!speedInTitle) - document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("speedInBrowserTitleBarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; // After showing/hiding the toolbar + status bar window.qBittorrent.Client.showSearchEngine(localPreferences.get("show_search_engine") !== "false"); @@ -460,7 +462,7 @@ window.addEventListener("DOMContentLoaded", (event) => { MochaUI.Desktop.setDesktopSize(); let syncMainDataLastResponseId = 0; - const serverState = {}; + const serverState: Record = {}; const removeTorrentFromCategoryList = (hash) => { if (hash === undefined) @@ -488,7 +490,7 @@ window.addEventListener("DOMContentLoaded", (event) => { let categoryData = window.qBittorrent.Client.categoryMap.get(category); if (categoryData === undefined) { // This should not happen categoryData = { - torrents: new Set() + torrents: new Set(), }; window.qBittorrent.Client.categoryMap.set(category, categoryData); } @@ -575,7 +577,7 @@ window.addEventListener("DOMContentLoaded", (event) => { const statusFilter = document.getElementById("statusFilterList"); const filterID = `${selectedStatus}_filter`; for (const status of statusFilter.children) - status.classList.toggle("selectedFilter", (status.id === filterID)); + status.classList.toggle("selectedFilter", status.id === filterID); }; const updateCategoryList = () => { @@ -586,12 +588,12 @@ window.addEventListener("DOMContentLoaded", (event) => { for (const el of [...categoryList.children]) el.remove(); - const categoryItemTemplate = document.getElementById("categoryFilterItem"); + const categoryItemTemplate = window.qBittorrent.Misc.getElementById("categoryFilterItem", "template"); const createLink = (category, text, count) => { - const categoryFilterItem = categoryItemTemplate.content.cloneNode(true).firstElementChild; + const categoryFilterItem = (categoryItemTemplate.content.cloneNode(true) as DocumentFragment).firstElementChild as HTMLLIElement; categoryFilterItem.id = category; - categoryFilterItem.classList.toggle("selectedFilter", (category === selectedCategory)); + categoryFilterItem.classList.toggle("selectedFilter", category === selectedCategory); const span = categoryFilterItem.firstElementChild; span.lastElementChild.textContent = `${text} (${count})`; @@ -599,18 +601,18 @@ window.addEventListener("DOMContentLoaded", (event) => { return categoryFilterItem; }; - const createCategoryTree = (category) => { - const stack = [{ parent: categoriesFragment, category: category }]; + const createCategoryTree = (category: Category) => { + const stack: { parent: HTMLElement | DocumentFragment; category: Category; }[] = [{ parent: categoriesFragment, category: category }]; while (stack.length > 0) { const { parent, category } = stack.pop(); const displayName = category.nameSegments.at(-1); const listItem = createLink(category.categoryName, displayName, category.categoryCount); - listItem.firstElementChild.style.paddingLeft = `${(category.nameSegments.length - 1) * 20 + 6}px`; + (listItem.firstElementChild as HTMLSpanElement).style.paddingLeft = `${(category.nameSegments.length - 1) * 20 + 6}px`; parent.appendChild(listItem); if (category.children.length > 0) { - listItem.querySelector(".categoryToggle").style.visibility = "visible"; + (listItem.querySelector(".categoryToggle") as HTMLButtonElement).style.visibility = "visible"; const unorderedList = document.createElement("ul"); listItem.appendChild(unorderedList); for (const subcategory of category.children.reverse()) @@ -628,7 +630,7 @@ window.addEventListener("DOMContentLoaded", (event) => { uncategorized += 1; } - const sortedCategories = []; + const sortedCategories: Category[] = []; for (const [category, categoryData] of window.qBittorrent.Client.categoryMap) { sortedCategories.push({ categoryName: category, @@ -637,8 +639,8 @@ window.addEventListener("DOMContentLoaded", (event) => { ...(useSubcategories && { children: [], isRoot: true, - forceExpand: localPreferences.get(`category_${category}_collapsed`) === null - }) + forceExpand: localPreferences.get(`category_${category}_collapsed`) === null, + }), }); } sortedCategories.sort((left, right) => { @@ -646,8 +648,7 @@ window.addEventListener("DOMContentLoaded", (event) => { const rightSegments = right.nameSegments; 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]); + const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(leftSegments[i], rightSegments[i]); if (compareResult !== 0) return compareResult; } @@ -663,8 +664,7 @@ window.addEventListener("DOMContentLoaded", (event) => { categoryList.classList.add("subcategories"); for (let i = 0; i < sortedCategories.length; ++i) { const category = sortedCategories[i]; - for (let j = (i + 1); - ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(`${category.categoryName}/`)); ++j) { + for (let j = i + 1; ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(`${category.categoryName}/`)); ++j) { const subcategory = sortedCategories[j]; category.categoryCount += subcategory.categoryCount; category.forceExpand ||= subcategory.forceExpand; @@ -697,7 +697,7 @@ window.addEventListener("DOMContentLoaded", (event) => { return; for (const category of categoryList.getElementsByTagName("li")) - category.classList.toggle("selectedFilter", (category.id === selectedCategory)); + category.classList.toggle("selectedFilter", category.id === selectedCategory); }; const updateTagList = () => { @@ -708,12 +708,12 @@ window.addEventListener("DOMContentLoaded", (event) => { for (const el of [...tagFilterList.children]) el.remove(); - const tagItemTemplate = document.getElementById("tagFilterItem"); + const tagItemTemplate = window.qBittorrent.Misc.getElementById("tagFilterItem", "template"); const createLink = (tag, text, count) => { - const tagFilterItem = tagItemTemplate.content.cloneNode(true).firstElementChild; + const tagFilterItem = (tagItemTemplate.content.cloneNode(true) as DocumentFragment).firstElementChild; tagFilterItem.id = tag; - tagFilterItem.classList.toggle("selectedFilter", (tag === selectedTag)); + tagFilterItem.classList.toggle("selectedFilter", tag === selectedTag); const span = tagFilterItem.firstElementChild; span.lastChild.textContent = `${text} (${count})`; @@ -734,7 +734,7 @@ window.addEventListener("DOMContentLoaded", (event) => { for (const [tag, torrents] of window.qBittorrent.Client.tagMap) { sortedTags.push({ tagName: tag, - tagSize: torrents.size + tagSize: torrents.size, }); } sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName)); @@ -751,7 +751,7 @@ window.addEventListener("DOMContentLoaded", (event) => { return; for (const tag of tagFilterList.children) - tag.classList.toggle("selectedFilter", (tag.id === selectedTag)); + tag.classList.toggle("selectedFilter", tag.id === selectedTag); }; const updateTrackerList = () => { @@ -762,26 +762,27 @@ window.addEventListener("DOMContentLoaded", (event) => { for (const el of [...trackerFilterList.children]) el.remove(); - const trackerItemTemplate = document.getElementById("trackerFilterItem"); + const trackerItemTemplate = window.qBittorrent.Misc.getElementById("trackerFilterItem", "template"); const createLink = (host, text, count) => { - const trackerFilterItem = trackerItemTemplate.content.cloneNode(true).firstElementChild; + const trackerFilterItem = (trackerItemTemplate.content.cloneNode(true) as DocumentFragment).firstElementChild; trackerFilterItem.id = host; - trackerFilterItem.classList.toggle("selectedFilter", (host === selectedTracker)); + trackerFilterItem.classList.toggle("selectedFilter", host === selectedTracker); const span = trackerFilterItem.firstElementChild; span.lastChild.textContent = `${text} (${count})`; + const img = span.lastElementChild as HTMLImageElement; switch (host) { case TRACKERS_ANNOUNCE_ERROR: case TRACKERS_ERROR: - span.lastElementChild.src = "images/tracker-error.svg"; + img.src = "images/tracker-error.svg"; break; case TRACKERS_TRACKERLESS: - span.lastElementChild.src = "images/trackerless.svg"; + img.src = "images/trackerless.svg"; break; case TRACKERS_WARNING: - span.lastElementChild.src = "images/tracker-warning.svg"; + img.src = "images/tracker-warning.svg"; break; } @@ -835,7 +836,7 @@ window.addEventListener("DOMContentLoaded", (event) => { return; for (const tracker of trackerFilterList.children) - tracker.classList.toggle("selectedFilter", (tracker.id === selectedTracker)); + tracker.classList.toggle("selectedFilter", tracker.id === selectedTracker); }; const statusSortOrder = Object.freeze({ @@ -857,204 +858,201 @@ window.addEventListener("DOMContentLoaded", (event) => { stoppedUP: 14, moving: 15, missingFiles: 16, - error: 17 + error: 17, }); let syncMainDataTimeoutID = -1; let syncRequestInProgress = false; const syncMainData = () => { syncRequestInProgress = true; - const url = new URL("api/v2/sync/maindata", window.location); + const url = new URL("api/v2/sync/maindata", window.location.href); url.search = new URLSearchParams({ - rid: syncMainDataLastResponseId - }); + rid: String(syncMainDataLastResponseId), + }).toString(); fetch(url, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { - if (response.ok) { - document.getElementById("error_div").textContent = ""; - - const responseJSON = await response.json(); - - clearTimeout(torrentsFilterInputTimer); - torrentsFilterInputTimer = -1; - - let torrentsTableSelectedRows; - let updateStatuses = false; - let updateCategories = false; - let updateTags = false; - let updateTrackers = false; - let updateTorrents = false; - const fullUpdate = (responseJSON["fullUpdate"] === true); - if (fullUpdate) { - torrentsTableSelectedRows = torrentsTable.selectedRowsIds(); - updateStatuses = true; - updateCategories = true; - updateTags = true; - updateTrackers = true; - updateTorrents = true; - torrentsTable.clear(); - window.qBittorrent.Client.categoryMap.clear(); - window.qBittorrent.Client.tagMap.clear(); - trackerMap.clear(); - } - if (responseJSON["rid"]) - syncMainDataLastResponseId = responseJSON["rid"]; - if (responseJSON["categories"]) { - for (const responseName in responseJSON["categories"]) { - if (!Object.hasOwn(responseJSON["categories"], responseName)) - continue; - - const responseData = responseJSON["categories"][responseName]; - const categoryData = window.qBittorrent.Client.categoryMap.get(responseName); - if (categoryData === undefined) { - window.qBittorrent.Client.categoryMap.set(responseName, { - savePath: responseData.savePath, - downloadPath: responseData.download_path ?? null, - torrents: new Set() - }); - } - else { - if (responseData.savePath !== undefined) - categoryData.savePath = responseData.savePath; - if (responseData.download_path !== undefined) - categoryData.downloadPath = responseData.download_path; - } + if (response.ok) { + document.getElementById("error_div").textContent = ""; + + const responseJSON = await response.json(); + + clearTimeout(torrentsFilterInputTimer); + torrentsFilterInputTimer = -1; + + let torrentsTableSelectedRows; + let updateStatuses = false; + let updateCategories = false; + let updateTags = false; + let updateTrackers = false; + let updateTorrents = false; + const fullUpdate = responseJSON["fullUpdate"] === true; + if (fullUpdate) { + torrentsTableSelectedRows = torrentsTable.selectedRowsIds(); + updateStatuses = true; + updateCategories = true; + updateTags = true; + updateTrackers = true; + updateTorrents = true; + torrentsTable.clear(); + window.qBittorrent.Client.categoryMap.clear(); + window.qBittorrent.Client.tagMap.clear(); + trackerMap.clear(); + } + if (responseJSON["rid"]) + syncMainDataLastResponseId = responseJSON["rid"]; + if (responseJSON["categories"]) { + for (const responseName in responseJSON["categories"]) { + if (!Object.hasOwn(responseJSON["categories"], responseName)) + continue; + + const responseData = responseJSON["categories"][responseName]; + const categoryData = window.qBittorrent.Client.categoryMap.get(responseName); + if (categoryData === undefined) { + window.qBittorrent.Client.categoryMap.set(responseName, { + savePath: responseData.savePath, + downloadPath: responseData.download_path ?? null, + torrents: new Set(), + }); } - updateCategories = true; - } - if (responseJSON["categories_removed"]) { - for (const category of responseJSON["categories_removed"]) - window.qBittorrent.Client.categoryMap.delete(category); - updateCategories = true; - } - if (responseJSON["tags"]) { - for (const tag of responseJSON["tags"]) { - if (!window.qBittorrent.Client.tagMap.has(tag)) - window.qBittorrent.Client.tagMap.set(tag, new Set()); + else { + if (responseData.savePath !== undefined) + categoryData.savePath = responseData.savePath; + if (responseData.download_path !== undefined) + categoryData.downloadPath = responseData.download_path; } - updateTags = true; } - if (responseJSON["tags_removed"]) { - for (const tag of responseJSON["tags_removed"]) - window.qBittorrent.Client.tagMap.delete(tag); - updateTags = true; + updateCategories = true; + } + if (responseJSON["categories_removed"]) { + for (const category of responseJSON["categories_removed"]) + window.qBittorrent.Client.categoryMap.delete(category); + updateCategories = true; + } + if (responseJSON["tags"]) { + for (const tag of responseJSON["tags"]) { + if (!window.qBittorrent.Client.tagMap.has(tag)) + window.qBittorrent.Client.tagMap.set(tag, new Set()); } - if (responseJSON["trackers"]) { - for (const [tracker, torrents] of Object.entries(responseJSON["trackers"])) { - const host = window.qBittorrent.Misc.getHost(tracker); - - let trackerListItem = trackerMap.get(host); - if (trackerListItem === undefined) { - trackerListItem = new Map(); - trackerMap.set(host, trackerListItem); - } - trackerListItem.set(tracker, new Set(torrents)); + updateTags = true; + } + if (responseJSON["tags_removed"]) { + for (const tag of responseJSON["tags_removed"]) + window.qBittorrent.Client.tagMap.delete(tag); + updateTags = true; + } + if (responseJSON["trackers"]) { + for (const [tracker, torrents] of Object.entries(responseJSON["trackers"])) { + const host = window.qBittorrent.Misc.getHost(tracker); + + let trackerListItem = trackerMap.get(host); + if (trackerListItem === undefined) { + trackerListItem = new Map(); + trackerMap.set(host, trackerListItem); } - updateTrackers = true; + trackerListItem.set(tracker, new Set(torrents as string[])); } - if (responseJSON["trackers_removed"]) { - for (let i = 0; i < responseJSON["trackers_removed"].length; ++i) { - const tracker = responseJSON["trackers_removed"][i]; - const host = window.qBittorrent.Misc.getHost(tracker); - - const trackerTorrentMap = trackerMap.get(host); - if (trackerTorrentMap !== undefined) { - trackerTorrentMap.delete(tracker); - // Remove unused trackers - if (trackerTorrentMap.size === 0) { - trackerMap.delete(host); - if (selectedTracker === host) { - selectedTracker = TRACKERS_ALL; - localPreferences.set("selected_tracker", selectedTracker); - } + updateTrackers = true; + } + if (responseJSON["trackers_removed"]) { + for (let i = 0; i < responseJSON["trackers_removed"].length; ++i) { + const tracker = responseJSON["trackers_removed"][i]; + const host = window.qBittorrent.Misc.getHost(tracker); + + const trackerTorrentMap = trackerMap.get(host); + if (trackerTorrentMap !== undefined) { + trackerTorrentMap.delete(tracker); + // Remove unused trackers + if (trackerTorrentMap.size === 0) { + trackerMap.delete(host); + if (selectedTracker === host) { + selectedTracker = TRACKERS_ALL; + localPreferences.set("selected_tracker", selectedTracker); } } } - updateTrackers = true; } - if (responseJSON["torrents"]) { - for (const key in responseJSON["torrents"]) { - if (!Object.hasOwn(responseJSON["torrents"], key)) - continue; - - responseJSON["torrents"][key]["hash"] = key; - responseJSON["torrents"][key]["rowId"] = key; - if (responseJSON["torrents"][key]["state"]) { - const state = responseJSON["torrents"][key]["state"]; - responseJSON["torrents"][key]["status"] = state; - responseJSON["torrents"][key]["_statusOrder"] = statusSortOrder[state]; - updateStatuses = true; - } - torrentsTable.updateRowData(responseJSON["torrents"][key]); - if (addTorrentToCategoryList(responseJSON["torrents"][key])) - updateCategories = true; - if (addTorrentToTagList(responseJSON["torrents"][key])) - updateTags = true; - updateTrackers = true; - updateTorrents = true; + updateTrackers = true; + } + if (responseJSON["torrents"]) { + for (const key in responseJSON["torrents"]) { + if (!Object.hasOwn(responseJSON["torrents"], key)) + continue; + + responseJSON["torrents"][key]["hash"] = key; + responseJSON["torrents"][key]["rowId"] = key; + if (responseJSON["torrents"][key]["state"]) { + const state = responseJSON["torrents"][key]["state"]; + responseJSON["torrents"][key]["status"] = state; + responseJSON["torrents"][key]["_statusOrder"] = statusSortOrder[state]; + updateStatuses = true; } - } - if (responseJSON["torrents_removed"]) { - responseJSON["torrents_removed"].each((hash) => { - torrentsTable.removeRow(hash); - removeTorrentFromCategoryList(hash); - updateCategories = true; // Always to update All category - removeTorrentFromTagList(hash); - updateTags = true; // Always to update All tag - updateTrackers = true; - }); + torrentsTable.updateRowData(responseJSON["torrents"][key]); + if (addTorrentToCategoryList(responseJSON["torrents"][key])) + updateCategories = true; + if (addTorrentToTagList(responseJSON["torrents"][key])) + updateTags = true; + updateTrackers = true; updateTorrents = true; - updateStatuses = true; } + } + if (responseJSON["torrents_removed"]) { + responseJSON["torrents_removed"].each((hash) => { + torrentsTable.removeRow(hash); + removeTorrentFromCategoryList(hash); + updateCategories = true; // Always to update All category + removeTorrentFromTagList(hash); + updateTags = true; // Always to update All tag + updateTrackers = true; + }); + updateTorrents = true; + updateStatuses = true; + } - // don't update the table unnecessarily - if (updateTorrents) - torrentsTable.updateTable(fullUpdate); + // don't update the table unnecessarily + if (updateTorrents) + torrentsTable.updateTable(fullUpdate); - if (responseJSON["server_state"]) { - const tmp = responseJSON["server_state"]; - for (const k in tmp) { - if (!Object.hasOwn(tmp, k)) - continue; - serverState[k] = tmp[k]; - } - processServerState(); + if (responseJSON["server_state"]) { + const tmp = responseJSON["server_state"]; + for (const k in tmp) { + if (!Object.hasOwn(tmp, k)) + continue; + serverState[k] = tmp[k]; } + processServerState(); + } - if (updateStatuses) - updateFiltersList(); - - if (updateCategories) { - updateCategoryList(); - window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(window.qBittorrent.Client.categoryMap); - } - if (updateTags) { - updateTagList(); - window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(window.qBittorrent.Client.tagMap); - } - if (updateTrackers) - updateTrackerList(); + if (updateStatuses) + updateFiltersList(); - if (fullUpdate) - // re-select previously selected rows - torrentsTable.reselectRows(torrentsTableSelectedRows); + if (updateCategories) { + updateCategoryList(); + window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(window.qBittorrent.Client.categoryMap); } + if (updateTags) { + updateTagList(); + window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(window.qBittorrent.Client.tagMap); + } + if (updateTrackers) + updateTrackerList(); - syncRequestInProgress = false; - syncData(window.qBittorrent.Client.getSyncMainDataInterval()); - }, - (error) => { - const errorDiv = document.getElementById("error_div"); - if (errorDiv) - errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; - syncRequestInProgress = false; - syncData(document.hidden - ? (window.qBittorrent.Cache.preferences.get().web_ui_session_timeout * 1000) / 2 - : 2000); - }); + if (fullUpdate) + // re-select previously selected rows + torrentsTable.reselectRows(torrentsTableSelectedRows); + } + + syncRequestInProgress = false; + syncData(window.qBittorrent.Client.getSyncMainDataInterval()); + }, (error) => { + const errorDiv = document.getElementById("error_div"); + if (errorDiv) + errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; + syncRequestInProgress = false; + syncData(document.hidden ? (window.qBittorrent.Cache.preferences.get().web_ui_session_timeout * 1000) / 2 : 2000); + }); }; updateMainData = () => { @@ -1072,7 +1070,7 @@ window.addEventListener("DOMContentLoaded", (event) => { if (window.qBittorrent.Client.isStopped()) return; - syncMainDataTimeoutID = syncMainData.delay(delay); + syncMainDataTimeoutID = window.setTimeout(syncMainData, delay); }; const processServerState = () => { @@ -1088,10 +1086,10 @@ window.addEventListener("DOMContentLoaded", (event) => { document.getElementById("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))) - : "") + ? ("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(); document.getElementById("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)); @@ -1133,21 +1131,22 @@ window.addEventListener("DOMContentLoaded", (event) => { window.qBittorrent.Statistics.save(serverState); window.qBittorrent.Statistics.render(); + const connectionStatusImg = document.getElementById("connectionStatus") as HTMLImageElement; switch (serverState.connection_status) { case "connected": - document.getElementById("connectionStatus").src = "images/connected.svg"; - document.getElementById("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]"; - document.getElementById("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]"; + connectionStatusImg.src = "images/connected.svg"; + connectionStatusImg.alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]"; + connectionStatusImg.title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]"; break; case "firewalled": - document.getElementById("connectionStatus").src = "images/firewalled.svg"; - document.getElementById("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]"; - document.getElementById("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]"; + connectionStatusImg.src = "images/firewalled.svg"; + connectionStatusImg.alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]"; + connectionStatusImg.title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]"; break; default: - document.getElementById("connectionStatus").src = "images/disconnected.svg"; - document.getElementById("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]"; - document.getElementById("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]"; + connectionStatusImg.src = "images/disconnected.svg"; + connectionStatusImg.alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]"; + connectionStatusImg.title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]"; break; } @@ -1187,15 +1186,16 @@ window.addEventListener("DOMContentLoaded", (event) => { }; const updateAltSpeedIcon = (enabled) => { + const alternativeSpeedLimitsImg = window.qBittorrent.Misc.getElementById("alternativeSpeedLimits", "image"); if (enabled) { - document.getElementById("alternativeSpeedLimits").src = "images/slow.svg"; - document.getElementById("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]"; - document.getElementById("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]"; + alternativeSpeedLimitsImg.src = "images/slow.svg"; + alternativeSpeedLimitsImg.alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]"; + alternativeSpeedLimitsImg.title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]"; } else { - document.getElementById("alternativeSpeedLimits").src = "images/slow_off.svg"; - document.getElementById("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]"; - document.getElementById("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]"; + alternativeSpeedLimitsImg.src = "images/slow_off.svg"; + alternativeSpeedLimitsImg.alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]"; + alternativeSpeedLimitsImg.title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]"; } }; @@ -1204,8 +1204,8 @@ window.addEventListener("DOMContentLoaded", (event) => { updateAltSpeedIcon(!alternativeSpeedLimits); fetch("api/v2/transfer/toggleSpeedLimitsMode", { - method: "POST" - }) + method: "POST", + }) .then((response) => { if (!response.ok) { // Restore icon in case of failure @@ -1218,18 +1218,22 @@ window.addEventListener("DOMContentLoaded", (event) => { }); }); - document.getElementById("DlInfos").addEventListener("click", (event) => { globalDownloadLimitFN(); }); - document.getElementById("UpInfos").addEventListener("click", (event) => { globalUploadLimitFN(); }); + document.getElementById("DlInfos").addEventListener("click", (event) => { + globalDownloadLimitFN(); + }); + document.getElementById("UpInfos").addEventListener("click", (event) => { + globalUploadLimitFN(); + }); document.getElementById("showTopToolbarLink").addEventListener("click", (e) => { showTopToolbar = !showTopToolbar; localPreferences.set("show_top_toolbar", showTopToolbar.toString()); if (showTopToolbar) { - document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "1"; + (document.getElementById("showTopToolbarLink").firstElementChild as HTMLImageElement).style.opacity = "1"; document.getElementById("mochaToolbar").classList.remove("invisible"); } else { - document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showTopToolbarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("mochaToolbar").classList.add("invisible"); } MochaUI.Desktop.setDesktopSize(); @@ -1239,11 +1243,11 @@ window.addEventListener("DOMContentLoaded", (event) => { showStatusBar = !showStatusBar; localPreferences.set("show_status_bar", showStatusBar.toString()); if (showStatusBar) { - document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "1"; + (document.getElementById("showStatusBarLink").firstElementChild as HTMLImageElement).style.opacity = "1"; document.getElementById("desktopFooterWrapper").classList.remove("invisible"); } else { - document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showStatusBarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("desktopFooterWrapper").classList.add("invisible"); } MochaUI.Desktop.setDesktopSize(); @@ -1265,8 +1269,7 @@ window.addEventListener("DOMContentLoaded", (event) => { 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"); + navigator.registerProtocolHandler("magnet", templateUrl); }; document.getElementById("registerMagnetHandlerLink").addEventListener("click", (e) => { registerMagnetHandler(); @@ -1276,12 +1279,12 @@ window.addEventListener("DOMContentLoaded", (event) => { showFiltersSidebar = !showFiltersSidebar; localPreferences.set("show_filters_sidebar", showFiltersSidebar.toString()); if (showFiltersSidebar) { - document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "1"; + (document.getElementById("showFiltersSidebarLink").firstElementChild as HTMLImageElement).style.opacity = "1"; document.getElementById("filtersColumn").classList.remove("invisible"); document.getElementById("filtersColumn_handle").classList.remove("invisible"); } else { - document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showFiltersSidebarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("filtersColumn").classList.add("invisible"); document.getElementById("filtersColumn_handle").classList.add("invisible"); } @@ -1292,9 +1295,9 @@ window.addEventListener("DOMContentLoaded", (event) => { speedInTitle = !speedInTitle; localPreferences.set("speed_in_browser_title_bar", speedInTitle.toString()); if (speedInTitle) - document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "1"; + (document.getElementById("speedInBrowserTitleBarLink").firstElementChild as HTMLImageElement).style.opacity = "1"; else - document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0"; + (document.getElementById("speedInBrowserTitleBarLink").firstElementChild as HTMLImageElement).style.opacity = "0"; processServerState(); }); @@ -1318,42 +1321,42 @@ window.addEventListener("DOMContentLoaded", (event) => { const updateTabDisplay = () => { if (window.qBittorrent.Client.isShowRssReader()) { - document.getElementById("showRssReaderLink").firstElementChild.style.opacity = "1"; + (document.getElementById("showRssReaderLink").firstElementChild as HTMLImageElement).style.opacity = "1"; document.getElementById("mainWindowTabs").classList.remove("invisible"); document.getElementById("rssTabLink").classList.remove("invisible"); if (!MochaUI.Panels.instances.RssPanel) addRssPanel(); } else { - document.getElementById("showRssReaderLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showRssReaderLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("rssTabLink").classList.add("invisible"); if (document.getElementById("rssTabLink").classList.contains("selected")) document.getElementById("transfersTabLink").click(); } if (window.qBittorrent.Client.isShowSearchEngine()) { - document.getElementById("showSearchEngineLink").firstElementChild.style.opacity = "1"; + (document.getElementById("showSearchEngineLink").firstElementChild as HTMLImageElement).style.opacity = "1"; document.getElementById("mainWindowTabs").classList.remove("invisible"); document.getElementById("searchTabLink").classList.remove("invisible"); if (!MochaUI.Panels.instances.SearchPanel) addSearchPanel(); } else { - document.getElementById("showSearchEngineLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showSearchEngineLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("searchTabLink").classList.add("invisible"); if (document.getElementById("searchTabLink").classList.contains("selected")) document.getElementById("transfersTabLink").click(); } if (window.qBittorrent.Client.isShowLogViewer()) { - document.getElementById("showLogViewerLink").firstElementChild.style.opacity = "1"; + (document.getElementById("showLogViewerLink").firstElementChild as HTMLImageElement).style.opacity = "1"; document.getElementById("mainWindowTabs").classList.remove("invisible"); document.getElementById("logTabLink").classList.remove("invisible"); if (!MochaUI.Panels.instances.LogPanel) addLogPanel(); } else { - document.getElementById("showLogViewerLink").firstElementChild.style.opacity = "0"; + (document.getElementById("showLogViewerLink").firstElementChild as HTMLImageElement).style.opacity = "0"; document.getElementById("logTabLink").classList.add("invisible"); if (document.getElementById("logTabLink").classList.contains("selected")) document.getElementById("transfersTabLink").click(); @@ -1364,7 +1367,9 @@ window.addEventListener("DOMContentLoaded", (event) => { document.getElementById("mainWindowTabs").classList.add("invisible"); }; - document.getElementById("StatisticsLink").addEventListener("click", (event) => { StatisticsLinkFN(); }); + document.getElementById("StatisticsLink").addEventListener("click", (event) => { + StatisticsLinkFN(); + }); // main window tabs @@ -1514,7 +1519,7 @@ window.addEventListener("DOMContentLoaded", (event) => { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, loadMethod: "xhr", contentURL: "views/search.html?v=${CACHEID}", @@ -1526,7 +1531,7 @@ window.addEventListener("DOMContentLoaded", (event) => { }, content: "", column: "searchTabColumn", - height: null + height: null, }); }; @@ -1539,7 +1544,7 @@ window.addEventListener("DOMContentLoaded", (event) => { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, loadMethod: "xhr", contentURL: "views/rss.html?v=${CACHEID}", @@ -1548,7 +1553,7 @@ window.addEventListener("DOMContentLoaded", (event) => { }, content: "", column: "rssTabColumn", - height: null + height: null, }); }; @@ -1561,7 +1566,7 @@ window.addEventListener("DOMContentLoaded", (event) => { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, loadMethod: "xhr", contentURL: "views/log.html?v=${CACHEID}", @@ -1587,7 +1592,7 @@ window.addEventListener("DOMContentLoaded", (event) => { collapsible: false, content: "", column: "logTabColumn", - height: null + height: null, }); }; @@ -1599,7 +1604,7 @@ window.addEventListener("DOMContentLoaded", (event) => { const url = decodeURIComponent(location.hash.substring(downloadHash.length)); // Remove the processed hash from the URL - history.replaceState("", document.title, (location.pathname + location.search)); + history.replaceState("", document.title, location.pathname + location.search); showDownloadPage([url]); }; @@ -1611,7 +1616,7 @@ window.addEventListener("DOMContentLoaded", (event) => { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, loadMethod: "xhr", contentURL: "views/transferlist.html?v=${CACHEID}", @@ -1621,17 +1626,17 @@ window.addEventListener("DOMContentLoaded", (event) => { }, column: "mainColumn", onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { - const isHidden = (Number.parseInt(document.getElementById("propertiesPanel").style.height, 10) === 0); + const isHidden = Number.parseInt(document.getElementById("propertiesPanel").style.height, 10) === 0; if (!isHidden) saveColumnSizes(); }), - height: null + height: null, }); let prop_h = localPreferences.get("properties_height_rel"); if (prop_h !== null) - prop_h = Number(prop_h) * Window.getSize().y; + prop_h = Number(prop_h) * document.getSize().y; else - prop_h = Window.getSize().y / 2; + prop_h = document.getSize().y / 2; new MochaUI.Panel({ id: "propertiesPanel", title: "Panel", @@ -1639,7 +1644,7 @@ window.addEventListener("DOMContentLoaded", (event) => { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, contentURL: "views/properties.html?v=${CACHEID}", require: { @@ -1648,7 +1653,7 @@ window.addEventListener("DOMContentLoaded", (event) => { "scripts/prop-trackers.js?v=${CACHEID}", "scripts/prop-peers.js?v=${CACHEID}", "scripts/prop-webseeds.js?v=${CACHEID}", - "scripts/prop-files.js?v=${CACHEID}" + "scripts/prop-files.js?v=${CACHEID}", ], onload: () => { updatePropertiesPanel = () => { @@ -1670,7 +1675,7 @@ window.addEventListener("DOMContentLoaded", (event) => { break; } }; - } + }, }, tabsURL: "views/propertiesToolbar.html?v=${CACHEID}", tabsOnload: () => {}, // must be included, otherwise panel won't load properly @@ -1723,25 +1728,35 @@ window.addEventListener("DOMContentLoaded", (event) => { document.getElementById("torrentFilesFilterToolbar").classList.remove("invisible"); }, column: "mainColumn", - height: prop_h + height: prop_h, }); // listen for changes to torrentsFilterInput let torrentsFilterInputTimer = -1; document.getElementById("torrentsFilterInput").addEventListener("input", (event) => { clearTimeout(torrentsFilterInputTimer); - torrentsFilterInputTimer = setTimeout(() => { + torrentsFilterInputTimer = window.setTimeout(() => { torrentsFilterInputTimer = -1; torrentsTable.updateTable(); }, window.qBittorrent.Misc.FILTER_INPUT_DELAY); }); - document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { torrentsTable.updateTable(); }); + document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { + torrentsTable.updateTable(); + }); - document.getElementById("transfersTabLink").addEventListener("click", (event) => { showTransfersTab(); }); - document.getElementById("searchTabLink").addEventListener("click", (event) => { showSearchTab(); }); - document.getElementById("rssTabLink").addEventListener("click", (event) => { showRssTab(); }); - document.getElementById("logTabLink").addEventListener("click", (event) => { showLogTab(); }); + document.getElementById("transfersTabLink").addEventListener("click", (event) => { + showTransfersTab(); + }); + document.getElementById("searchTabLink").addEventListener("click", (event) => { + showSearchTab(); + }); + document.getElementById("rssTabLink").addEventListener("click", (event) => { + showRssTab(); + }); + document.getElementById("logTabLink").addEventListener("click", (event) => { + showLogTab(); + }); updateTabDisplay(); const registerDragAndDrop = () => { @@ -1790,20 +1805,21 @@ window.addEventListener("DOMContentLoaded", (event) => { }); for (const url of urls) - qBittorrent.Client.createAddTorrentWindow(url, url); + window.qBittorrent.Client.createAddTorrentWindow(url, url); } }); }; registerDragAndDrop(); window.addEventListener("keydown", (event) => { + const target = event.target as HTMLElement; switch (event.key) { case "a": case "A": if (event.ctrlKey || event.metaKey) { - if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA")) + if ((target.nodeName === "INPUT") || (target.nodeName === "TEXTAREA")) return; - if (event.target.isContentEditable) + if (target.isContentEditable) return; event.preventDefault(); torrentsTable.selectAll(); @@ -1811,25 +1827,25 @@ window.addEventListener("DOMContentLoaded", (event) => { break; case "Delete": - if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA")) + if ((target.nodeName === "INPUT") || (target.nodeName === "TEXTAREA")) return; - if (event.target.isContentEditable) + if (target.isContentEditable) return; event.preventDefault(); deleteSelectedTorrentsFN(event.shiftKey); break; case "Escape": { - if (event.target.isContentEditable) + if (target.isContentEditable) return; event.preventDefault(); - const modalInstances = Object.values(MochaUI.Windows.instances); + const modalInstances: Window[] = Object.values(MochaUI.Windows.instances); if (modalInstances.length <= 0) return; // MochaUI.currentModal does not update after a modal is closed - const focusedModal = modalInstances.find((modal) => { + const focusedModal = modalInstances.find((modal: any) => { return modal.windowEl.hasClass("isFocused"); }); if (focusedModal !== undefined) @@ -1840,9 +1856,9 @@ window.addEventListener("DOMContentLoaded", (event) => { case "f": case "F": if (event.ctrlKey || event.metaKey) { - if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA")) + if ((target.nodeName === "INPUT") || (target.nodeName === "TEXTAREA")) return; - if (event.target.isContentEditable) + if (target.isContentEditable) return; const logsFilterElem = document.getElementById("filterTextInput"); diff --git a/src/webui/www/private/scripts/color-scheme.js b/src/webui/www/private/scripts/color-scheme.ts similarity index 92% rename from src/webui/www/private/scripts/color-scheme.js rename to src/webui/www/private/scripts/color-scheme.ts index 3e41ddc523fe..1acd4cbd6915 100644 --- a/src/webui/www/private/scripts/color-scheme.js +++ b/src/webui/www/private/scripts/color-scheme.ts @@ -26,10 +26,8 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; -window.qBittorrent.ColorScheme ??= (() => { +const colorSchemeModule = (() => { const exports = () => { return { update, @@ -44,13 +42,15 @@ window.qBittorrent.ColorScheme ??= (() => { const colorScheme = localPreferences.get("color_scheme"); const validScheme = (colorScheme === "light") || (colorScheme === "dark"); const isDark = colorSchemeQuery.matches; - root.classList.toggle("dark", ((!validScheme && isDark) || (colorScheme === "dark"))); + root.classList.toggle("dark", (!validScheme && isDark) || (colorScheme === "dark")); }; colorSchemeQuery.addEventListener("change", update); return exports(); })(); + +window.qBittorrent.ColorScheme ??= colorSchemeModule; Object.freeze(window.qBittorrent.ColorScheme); window.qBittorrent.ColorScheme.update(); diff --git a/src/webui/www/private/scripts/contextmenu.js b/src/webui/www/private/scripts/contextmenu.ts similarity index 89% rename from src/webui/www/private/scripts/contextmenu.js rename to src/webui/www/private/scripts/contextmenu.ts index 339dc6466acc..62e0950d8612 100644 --- a/src/webui/www/private/scripts/contextmenu.js +++ b/src/webui/www/private/scripts/contextmenu.ts @@ -26,10 +26,8 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; -window.qBittorrent.ContextMenu ??= (() => { +const contextMenuModule = (() => { const exports = () => { return { ContextMenu: ContextMenu, @@ -41,12 +39,17 @@ window.qBittorrent.ContextMenu ??= (() => { SearchPluginsTableContextMenu: SearchPluginsTableContextMenu, RssFeedContextMenu: RssFeedContextMenu, RssArticleContextMenu: RssArticleContextMenu, - RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu + RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu, }; }; let lastShownContextMenu = null; class ContextMenu { + options: Record; + menu: HTMLUListElement; + touchStartAt: number; + touchStartEvent: any; + constructor(options) { this.options = { actions: {}, @@ -55,17 +58,17 @@ window.qBittorrent.ContextMenu ??= (() => { targets: "body", offsets: { x: 0, - y: 0 + y: 0, }, onShow: () => {}, onHide: () => {}, onClick: () => {}, touchTimer: 600, - ...options + ...options, }; // option diffs menu - this.menu = document.getElementById(this.options.menu); + this.menu = window.qBittorrent.Misc.getElementById(this.options.menu, "ul"); // hide and begin the listener this.hide().startListener(); @@ -109,13 +112,13 @@ window.qBittorrent.ContextMenu ??= (() => { for (const ul of uls) { if (ul.classList.contains("scrollableMenu")) ul.style.maxHeight = `${scrollableMenuMaxHeight}px`; - const rectParent = ul.parentNode.getBoundingClientRect(); + const rectParent = (ul.parentNode as HTMLElement).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); + xPos -= ul.offsetWidth + rectParent.width - 2; if ((yPos + ul.offsetHeight) > document.documentElement.clientHeight) yPos = document.documentElement.clientHeight - ul.offsetHeight; if (xPos < 0) @@ -194,13 +197,13 @@ window.qBittorrent.ContextMenu ??= (() => { /* menu items */ this.menu.addEventListener("click", (e) => { - const menuItem = e.target.closest("li"); + const menuItem = (e.target as HTMLElement).closest("li"); if (!menuItem) return; e.preventDefault(); if (!menuItem.classList.contains("disabled")) { - const anchor = menuItem.firstElementChild; + const anchor = menuItem.firstElementChild as HTMLAnchorElement; this.execute(anchor.href.split("#")[1], this.options.element); this.options.onClick.call(this, anchor, e); } @@ -219,7 +222,7 @@ window.qBittorrent.ContextMenu ??= (() => { updateMenuItems() {} // show menu - show(trigger) { + show() { if (lastShownContextMenu && (lastShownContextMenu !== this)) lastShownContextMenu.hide(); this.menu.classList.add("visible"); @@ -229,7 +232,7 @@ window.qBittorrent.ContextMenu ??= (() => { } // hide the menu - hide(trigger) { + hide() { if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) { this.menu.classList.remove("visible"); this.options.onHide.call(this); @@ -238,24 +241,23 @@ window.qBittorrent.ContextMenu ??= (() => { } setItemChecked(item, checked) { - this.menu.querySelector(`a[href$="${item}"]`).firstElementChild.style.opacity = - checked ? "1" : "0"; + (this.menu.querySelector(`a[href$="${item}"]`).firstElementChild as HTMLElement).style.opacity = checked ? "1" : "0"; return this; } getItemChecked(item) { - return this.menu.querySelector(`a[href$="${item}"]`).firstElementChild.style.opacity !== "0"; + return (this.menu.querySelector(`a[href$="${item}"]`).firstElementChild as HTMLElement).style.opacity !== "0"; } // hide an item hideItem(item) { - this.menu.querySelector(`a[href$="${item}"]`).parentNode.classList.add("invisible"); + (this.menu.querySelector(`a[href$="${item}"]`).parentNode as HTMLElement).classList.add("invisible"); return this; } // show an item showItem(item) { - this.menu.querySelector(`a[href$="${item}"]`).parentNode.classList.remove("invisible"); + (this.menu.querySelector(`a[href$="${item}"]`).parentNode as HTMLElement).classList.remove("invisible"); return this; } @@ -286,6 +288,8 @@ window.qBittorrent.ContextMenu ??= (() => { } class FilterListContextMenu extends ContextMenu { + torrentObserver: MutationObserver; + constructor(options) { super(options); this.torrentObserver = new MutationObserver((records, observer) => { @@ -368,12 +372,12 @@ window.qBittorrent.ContextMenu ??= (() => { const torrentTags = data["tags"].split(", "); for (const tag of torrentTags) { const count = tagCount.get(tag); - tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1)); + tagCount.set(tag, (count !== undefined) ? (count + 1) : 1); } const torrentCategory = data["category"]; const count = categoryCount.get(torrentCategory); - categoryCount.set(torrentCategory, ((count !== undefined) ? (count + 1) : 1)); + categoryCount.set(torrentCategory, (count !== undefined) ? (count + 1) : 1); } // hide renameFiles when more than 1 torrent is selected @@ -383,9 +387,7 @@ window.qBittorrent.ContextMenu ??= (() => { this.showItem("rename"); // hide renameFiles when metadata hasn't been downloaded yet - metadata_downloaded - ? this.showItem("renameFiles") - : this.hideItem("renameFiles"); + metadata_downloaded ? this.showItem("renameFiles") : this.hideItem("renameFiles"); } else { this.hideItem("renameFiles"); @@ -394,17 +396,17 @@ window.qBittorrent.ContextMenu ??= (() => { if (all_are_downloaded) { this.hideItem("downloadLimit"); - this.menu.querySelector("a[href$=uploadLimit]").parentNode.classList.add("separator"); + (this.menu.querySelector("a[href$=uploadLimit]").parentNode as HTMLElement).classList.add("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); + 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; - this.menu.querySelector("a[href$=firstLastPiecePrio]").parentNode.classList.toggle("separator", (!show_seq_dl && show_f_l_piece_prio)); + (this.menu.querySelector("a[href$=firstLastPiecePrio]").parentNode as HTMLElement).classList.toggle("separator", !show_seq_dl && show_f_l_piece_prio); if (show_seq_dl) this.showItem("sequentialDownload"); @@ -420,7 +422,7 @@ window.qBittorrent.ContextMenu ??= (() => { this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio); this.showItem("downloadLimit"); - this.menu.querySelector("a[href$=uploadLimit]").parentNode.classList.remove("separator"); + (this.menu.querySelector("a[href$=uploadLimit]").parentNode as HTMLElement).classList.remove("separator"); this.hideItem("superSeeding"); } @@ -441,19 +443,19 @@ window.qBittorrent.ContextMenu ??= (() => { const contextTagList = document.getElementById("contextTagList"); for (const tag of window.qBittorrent.Client.tagMap.keys()) { - const checkbox = contextTagList.querySelector(`a[href="#Tag/${tag}"] input[type="checkbox"]`); + const checkbox = contextTagList.querySelector(`a[href="#Tag/${tag}"] input[type="checkbox"]`) as HTMLInputElement; const count = tagCount.get(tag); - const hasCount = (count !== undefined); - const isLesser = (count < selectedRows.length); - checkbox.indeterminate = (hasCount ? isLesser : false); - checkbox.checked = (hasCount ? !isLesser : false); + const hasCount = count !== undefined; + const isLesser = count < selectedRows.length; + checkbox.indeterminate = hasCount ? isLesser : false; + checkbox.checked = hasCount ? !isLesser : false; } const contextCategoryList = document.getElementById("contextCategoryList"); for (const category of window.qBittorrent.Client.categoryMap.keys()) { const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category}"] img`); const count = categoryCount.get(category); - const isEqual = ((count !== undefined) && (count === selectedRows.length)); + const isEqual = (count !== undefined) && (count === selectedRows.length); categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual); } } @@ -479,8 +481,12 @@ window.qBittorrent.ContextMenu ??= (() => { return item; }; - contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", (event) => { torrentNewCategoryFN(); })); - contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", (event) => { torrentSetCategoryFN(""); })); + contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", (event) => { + torrentNewCategoryFN(); + })); + contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", (event) => { + torrentSetCategoryFN(""); + })); const sortedCategories = [...categories.keys()]; sortedCategories.sort(window.qBittorrent.Misc.naturalSortCollator.compare); @@ -529,8 +535,12 @@ window.qBittorrent.ContextMenu ??= (() => { return item; }; - contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", (event) => { torrentAddTagsFN(); })); - contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", (event) => { torrentRemoveAllTagsFN(); })); + contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", (event) => { + torrentAddTagsFN(); + })); + contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", (event) => { + torrentRemoveAllTagsFN(); + })); const sortedTags = [...tags.keys()]; sortedTags.sort(window.qBittorrent.Misc.naturalSortCollator.compare); @@ -621,13 +631,13 @@ window.qBittorrent.ContextMenu ??= (() => { class SearchPluginsTableContextMenu extends ContextMenu { updateMenuItems() { - const enabledColumnIndex = (text) => { + const enabledColumnIndex = () => { const columns = document.querySelectorAll("#searchPluginsTableFixedHeaderRow th"); - return Array.prototype.findIndex.call(columns, (column => column.textContent === "Enabled")); + return Array.prototype.findIndex.call(columns, column => column.textContent === "Enabled"); }; this.showItem("Enabled"); - this.setItemChecked("Enabled", (this.options.element.children[enabledColumnIndex()].textContent === "Yes")); + this.setItemChecked("Enabled", this.options.element.children[enabledColumnIndex()].textContent === "Yes"); this.showItem("Uninstall"); } @@ -636,11 +646,11 @@ window.qBittorrent.ContextMenu ??= (() => { class RssFeedContextMenu extends ContextMenu { updateMenuItems() { const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds(); - this.menu.querySelector("a[href$=newSubscription]").parentNode.classList.add("separator"); + (this.menu.querySelector("a[href$=newSubscription]").parentNode as HTMLElement).classList.add("separator"); switch (selectedRows.length) { case 0: // remove separator on top of newSubscription entry to avoid double line - this.menu.querySelector("a[href$=newSubscription]").parentNode.classList.remove("separator"); + (this.menu.querySelector("a[href$=newSubscription]").parentNode as HTMLElement).classList.remove("separator"); // menu when nothing selected this.hideItem("update"); this.hideItem("markRead"); @@ -758,4 +768,6 @@ window.qBittorrent.ContextMenu ??= (() => { return exports(); })(); + +window.qBittorrent.ContextMenu ??= contextMenuModule; Object.freeze(window.qBittorrent.ContextMenu); diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.ts similarity index 95% rename from src/webui/www/private/scripts/dynamicTable.js rename to src/webui/www/private/scripts/dynamicTable.ts index b8e895ffb464..b06833b8bd2e 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.ts @@ -31,7 +31,8 @@ **************************************************************/ -"use strict"; +type FileCheckbox = HTMLInputElement & { state: number; indeterminate: boolean; }; +type BulkRenameFileCheckbox = HTMLInputElement & { state: "checked" | "unchecked" | "partial"; indeterminate: boolean; }; window.qBittorrent ??= {}; window.qBittorrent.DynamicTable ??= (() => { @@ -69,6 +70,29 @@ window.qBittorrent.DynamicTable ??= (() => { class DynamicTable { #DynamicTableHeaderContextMenuClass = null; + rowHeight = 26; + rows = new Map(); + cachedElements = []; + selectedRows = []; + columns = []; + renderedOffset = 0; + renderedHeight = 0; + currentHeaderAction = ""; + canResize = false; + previousTableHeight = 0; + + dynamicTableDivId = null; + dynamicTableFixedHeaderDivId = null; + dynamicTableDiv = null; + useVirtualList = null; + fixedTableHeader = null; + hiddenTableHeader = null; + table = null; + tableBody = null; + contextMenu = null; + headerContextMenu = null; + sortedColumn = null; + reverseSort = null; setup(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu, useVirtualList = false) { this.dynamicTableDivId = dynamicTableDivId; @@ -79,11 +103,6 @@ window.qBittorrent.DynamicTable ??= (() => { this.hiddenTableHeader = this.dynamicTableDiv.querySelector("thead tr"); this.table = this.dynamicTableDiv.querySelector("table"); this.tableBody = this.dynamicTableDiv.querySelector("tbody"); - this.rowHeight = 26; - this.rows = new Map(); - this.cachedElements = []; - this.selectedRows = []; - this.columns = []; this.contextMenu = contextMenu; this.sortedColumn = localPreferences.get(`sorted_column_${this.dynamicTableDivId}`, 0); this.reverseSort = localPreferences.get(`reverse_sort_${this.dynamicTableDivId}`, "0"); @@ -201,7 +220,7 @@ window.qBittorrent.DynamicTable ??= (() => { this.currentHeaderAction = ""; this.canResize = false; - const resetElementBorderStyle = (el, side) => { + const resetElementBorderStyle = (el, side = null) => { if ((side === "left") || (side !== "right")) el.style.borderLeft = ""; if ((side === "right") || (side !== "left")) @@ -259,7 +278,7 @@ window.qBittorrent.DynamicTable ??= (() => { borderChangeElement.style.borderRightWidth = "initial"; } - resetElementBorderStyle(borderChangeElement, ((changeBorderSide === "right") ? "left" : "right")); + resetElementBorderStyle(borderChangeElement, (changeBorderSide === "right") ? "left" : "right"); borderChangeElement.getSiblings('[class=""]').each((el) => { resetElementBorderStyle(el); @@ -364,19 +383,21 @@ window.qBittorrent.DynamicTable ??= (() => { th.makeResizable({ modifiers: { x: "", - y: "" + y: "", }, onBeforeStart: onBeforeStart, onStart: onStart, onDrag: onDrag, onComplete: onComplete, - onCancel: onCancel + onCancel: onCancel, }); } } setupDynamicTableHeaderContextMenuClass() { this.#DynamicTableHeaderContextMenuClass ??= class extends window.qBittorrent.ContextMenu.ContextMenu { + dynamicTable: DynamicTable; + updateMenuItems() { for (let i = 0; i < this.dynamicTable.columns.length; ++i) { if (this.dynamicTable.columns[i].caption === "") @@ -525,7 +546,8 @@ window.qBittorrent.DynamicTable ??= (() => { anchor.textContent = text; const spacer = document.createElement("span"); - spacer.style = "display: inline-block; width: calc(.5em + 16px);"; + spacer.style.display = "inline-block"; + spacer.style.width = "calc(.5em + 16px)"; anchor.prepend(spacer); const li = document.createElement("li"); @@ -547,8 +569,8 @@ window.qBittorrent.DynamicTable ??= (() => { menu: menuId, offsets: { x: 0, - y: 2 - } + y: 2, + }, }); this.headerContextMenu.dynamicTable = this; @@ -560,7 +582,7 @@ window.qBittorrent.DynamicTable ??= (() => { const column = {}; column["name"] = name; column["title"] = name; - column["visible"] = localPreferences.get(`column_${name}_visible_${this.dynamicTableDivId}`, (defaultVisible ? "1" : "0")); + column["visible"] = localPreferences.get(`column_${name}_visible_${this.dynamicTableDivId}`, defaultVisible ? "1" : "0"); column["force_hide"] = false; column["caption"] = caption; column["style"] = style; @@ -574,7 +596,7 @@ window.qBittorrent.DynamicTable ??= (() => { column["compareRows"] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); - if ((typeof(value1) === "number") && (typeof(value2) === "number")) + if ((typeof value1 === "number") && (typeof value2 === "number")) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; @@ -603,12 +625,12 @@ window.qBittorrent.DynamicTable ??= (() => { if ((val === null) || (val === undefined)) return; for (const v of val.split(",")) { - if ((v in this.columns) && (!columnsOrder.contains(v))) + if ((v in this.columns) && (!columnsOrder.includes(v))) columnsOrder.push(v); } for (let i = 0; i < this.columns.length; ++i) { - if (!columnsOrder.contains(this.columns[i].name)) + if (!columnsOrder.includes(this.columns[i].name)) columnsOrder.push(this.columns[i].name); } @@ -629,7 +651,7 @@ window.qBittorrent.DynamicTable ??= (() => { updateTableHeaders() { this.updateHeader(this.hiddenTableHeader); this.updateHeader(this.fixedTableHeader); - this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1")); + this.setSortedColumnIcon(this.sortedColumn, null, this.reverseSort === "1"); } updateHeader(header) { @@ -641,7 +663,7 @@ window.qBittorrent.DynamicTable ??= (() => { th.style.cssText = `width: ${this.columns[i].width}px; ${this.columns[i].style}`; th.columnName = this.columns[i].name; th.className = `column_${th.columnName}`; - th.classList.toggle("invisible", ((this.columns[i].visible === "0") || this.columns[i].force_hide)); + th.classList.toggle("invisible", (this.columns[i].visible === "0") || this.columns[i].force_hide); } } } @@ -689,7 +711,7 @@ window.qBittorrent.DynamicTable ??= (() => { else { // Toggle sort order this.reverseSort = reverse ?? (this.reverseSort === "0" ? "1" : "0"); - this.setSortedColumnIcon(column, null, (this.reverseSort === "1")); + this.setSortedColumnIcon(column, null, this.reverseSort === "1"); } localPreferences.set(`sorted_column_${this.dynamicTableDivId}`, column); localPreferences.set(`reverse_sort_${this.dynamicTableDivId}`, this.reverseSort); @@ -723,11 +745,11 @@ window.qBittorrent.DynamicTable ??= (() => { } isRowSelected(rowId) { - return this.selectedRows.contains(rowId); + return this.selectedRows.includes(rowId); } setupAltRow() { - const useAltRowColors = (localPreferences.get("use_alt_row_colors", "true") === "true"); + const useAltRowColors = localPreferences.get("use_alt_row_colors", "true") === "true"; if (useAltRowColors) document.getElementById(this.dynamicTableDivId).classList.add("altRowColors"); } @@ -740,7 +762,7 @@ window.qBittorrent.DynamicTable ??= (() => { } deselectAll() { - this.selectedRows.empty(); + this.selectedRows.length = 0; } selectRow(rowId) { @@ -750,7 +772,10 @@ window.qBittorrent.DynamicTable ??= (() => { } deselectRow(rowId) { - this.selectedRows.erase(rowId); + for (let i = this.selectedRows.length; i >= 0; --i) { + if (this.selectedRows[i] === rowId) + this.selectedRows.splice(i, 1); + } this.setRowClass(); this.onSelectedRowChanged(); } @@ -797,7 +822,7 @@ window.qBittorrent.DynamicTable ??= (() => { if (!this.rows.has(rowId)) { row = { full_data: {}, - rowId: rowId + rowId: rowId, }; this.rows.set(rowId, row); } @@ -834,7 +859,7 @@ window.qBittorrent.DynamicTable ??= (() => { } const column = this.columns[this.sortedColumn]; - const isReverseSort = (this.reverseSort === "0"); + const isReverseSort = this.reverseSort === "0"; filteredRows.sort((row1, row2) => { const result = column.compareRows(row1, row2); return isReverseSort ? result : -result; @@ -843,7 +868,7 @@ window.qBittorrent.DynamicTable ??= (() => { } getTrByRowId(rowId) { - return Array.prototype.find.call(this.getTrs(), (tr => tr.rowId === rowId)); + return Array.prototype.find.call(this.getTrs(), tr => tr.rowId === rowId); } updateTable(fullUpdate = false) { @@ -925,11 +950,11 @@ window.qBittorrent.DynamicTable ??= (() => { // how many rows can be shown in the visible area const visibleRowCount = Math.ceil(this.renderedHeight / this.rowHeight) + (extraRowCount * 2); // start position of visible rows, offsetted by renderedOffset - let startRow = Math.max((Math.trunc(this.renderedOffset / this.rowHeight) - extraRowCount), 0); + let startRow = Math.max(Math.trunc(this.renderedOffset / this.rowHeight) - extraRowCount, 0); // ensure startRow is even if ((startRow % 2) === 1) startRow = Math.max(0, startRow - 1); - const endRow = Math.min((startRow + visibleRowCount), rows.length); + const endRow = Math.min(startRow + visibleRowCount, rows.length); const elements = []; for (let i = startRow; i < endRow; ++i) { @@ -1011,7 +1036,11 @@ window.qBittorrent.DynamicTable ??= (() => { } removeRow(rowId) { - this.selectedRows.erase(rowId); + for (let i = this.selectedRows.length; i >= 0; --i) { + if (this.selectedRows[i] === rowId) + this.selectedRows.splice(i, 1); + } + this.rows.delete(rowId); if (this.useVirtualList) { this.rerender(); @@ -1055,7 +1084,7 @@ window.qBittorrent.DynamicTable ??= (() => { } selectNextRow() { - const visibleRows = Array.prototype.filter.call(this.getTrs(), (tr => !tr.classList.contains("invisible") && (tr.style.display !== "none"))); + const visibleRows = Array.prototype.filter.call(this.getTrs(), tr => !tr.classList.contains("invisible") && (tr.style.display !== "none")); const selectedRowId = this.getSelectedRowId(); let selectedIndex = -1; @@ -1066,7 +1095,7 @@ window.qBittorrent.DynamicTable ??= (() => { } } - const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1)); + const isLastRowSelected = selectedIndex >= (visibleRows.length - 1); if (!isLastRowSelected) { this.deselectAll(); @@ -1076,7 +1105,7 @@ window.qBittorrent.DynamicTable ??= (() => { } selectPreviousRow() { - const visibleRows = Array.prototype.filter.call(this.getTrs(), (tr => !tr.classList.contains("invisible") && (tr.style.display !== "none"))); + const visibleRows = Array.prototype.filter.call(this.getTrs(), tr => !tr.classList.contains("invisible") && (tr.style.display !== "none")); const selectedRowId = this.getSelectedRowId(); let selectedIndex = -1; @@ -1348,7 +1377,7 @@ window.qBittorrent.DynamicTable ??= (() => { // progress this.columns["progress"].updateTd = function(td, row) { const progress = this.getRowValue(row); - const progressFormatted = window.qBittorrent.Misc.toFixedPointString((progress * 100), 1); + const progressFormatted = window.qBittorrent.Misc.toFixedPointString(progress * 100, 1); const div = td.firstElementChild; if (div !== null) { @@ -1543,11 +1572,7 @@ window.qBittorrent.DynamicTable ??= (() => { this.columns["private"].updateTd = function(td, row) { const hasMetadata = row["full_data"].has_metadata; const isPrivate = this.getRowValue(row); - const string = hasMetadata - ? (isPrivate - ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" - : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]") - : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; + const string = hasMetadata ? (isPrivate ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]") : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; td.textContent = string; td.title = string; }; @@ -1596,7 +1621,7 @@ window.qBittorrent.DynamicTable ??= (() => { case "active": { let r; if (state === "stalledDL") - r = (row["full_data"].upspeed > 0); + r = row["full_data"].upspeed > 0; else r = (state === "metaDL") || (state === "forcedMetaDL") || (state === "downloading") || (state === "forcedDL") || (state === "uploading") || (state === "forcedUP"); if (r === inactive) @@ -1702,7 +1727,7 @@ window.qBittorrent.DynamicTable ??= (() => { } if ((filterTerms !== undefined) && (filterTerms !== null)) { - const filterBy = document.getElementById("torrentsFilterSelect").value; + const filterBy = window.qBittorrent.Misc.getElementById("torrentsFilterSelect", "select").value; const textToSearch = row["full_data"][filterBy].toLowerCase(); if (filterTerms instanceof RegExp) { if (!filterTerms.test(textToSearch)) @@ -1729,16 +1754,14 @@ window.qBittorrent.DynamicTable ??= (() => { getFilteredTorrentsHashes(filterName, category, tag, tracker) { const rowsHashes = []; - const useRegex = document.getElementById("torrentsFilterRegexBox").checked; - const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase(); + const useRegex = window.qBittorrent.Misc.getElementById("torrentsFilterRegexBox", "input").checked; + const filterText = window.qBittorrent.Misc.getElementById("torrentsFilterInput", "input").value.trim().toLowerCase(); let filterTerms; try { - filterTerms = (filterText.length > 0) - ? (useRegex ? new RegExp(filterText) : filterText.split(" ")) - : null; + filterTerms = (filterText.length > 0) ? (useRegex ? new RegExp(filterText) : filterText.split(" ")) : null; } catch (e) { // SyntaxError: Invalid regex pattern - return filteredRows; + return []; } for (const row of this.rows.values()) { @@ -1752,13 +1775,11 @@ window.qBittorrent.DynamicTable ??= (() => { getFilteredAndSortedRows() { const filteredRows = []; - const useRegex = document.getElementById("torrentsFilterRegexBox").checked; - const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase(); + const useRegex = window.qBittorrent.Misc.getElementById("torrentsFilterRegexBox", "input").checked; + const filterText = window.qBittorrent.Misc.getElementById("torrentsFilterInput", "input").value.trim().toLowerCase(); let filterTerms; try { - filterTerms = (filterText.length > 0) - ? (useRegex ? new RegExp(filterText) : filterText.split(" ")) - : null; + filterTerms = (filterText.length > 0) ? (useRegex ? new RegExp(filterText) : filterText.split(" ")) : null; } catch (e) { // SyntaxError: Invalid regex pattern return filteredRows; @@ -1772,7 +1793,7 @@ window.qBittorrent.DynamicTable ??= (() => { } const column = this.columns[this.sortedColumn]; - const isReverseSort = (this.reverseSort === "0"); + const isReverseSort = this.reverseSort === "0"; filteredRows.sort((row1, row2) => { const result = column.compareRows(row1, row2); return isReverseSort ? result : -result; @@ -1792,13 +1813,12 @@ window.qBittorrent.DynamicTable ??= (() => { const row = this.getRow(tr.rowId); const state = row["full_data"].state; - const prefKey = - (state !== "uploading") - && (state !== "stoppedUP") - && (state !== "forcedUP") - && (state !== "stalledUP") - && (state !== "queuedUP") - && (state !== "checkingUP") + const prefKey = (state !== "uploading") + && (state !== "stoppedUP") + && (state !== "forcedUP") + && (state !== "stalledUP") + && (state !== "queuedUP") + && (state !== "checkingUP") ? "dblclick_download" : "dblclick_complete"; @@ -1883,7 +1903,7 @@ window.qBittorrent.DynamicTable ??= (() => { // progress this.columns["progress"].updateTd = function(td, row) { const progress = this.getRowValue(row); - const progressFormatted = `${window.qBittorrent.Misc.toFixedPointString((progress * 100), 1)}%`; + const progressFormatted = `${window.qBittorrent.Misc.toFixedPointString(progress * 100, 1)}%`; td.textContent = progressFormatted; td.title = progressFormatted; }; @@ -1921,7 +1941,6 @@ window.qBittorrent.DynamicTable ??= (() => { td.textContent = value.replace(/\n/g, ";"); td.title = value; }; - } } @@ -1976,7 +1995,7 @@ window.qBittorrent.DynamicTable ??= (() => { return { min: minSize, - max: maxSize + max: maxSize, }; }; @@ -1992,7 +2011,7 @@ window.qBittorrent.DynamicTable ??= (() => { return { min: minSeeds, - max: maxSeeds + max: maxSeeds, }; }; @@ -2001,11 +2020,10 @@ window.qBittorrent.DynamicTable ??= (() => { const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" "); const sizeFilters = getSizeFilters(); const seedsFilters = getSeedsFilters(); - const searchInTorrentName = document.getElementById("searchInTorrentName").value === "names"; + const searchInTorrentName = window.qBittorrent.Misc.getElementById("searchInTorrentName", "input").value === "names"; if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0) || (window.qBittorrent.Search.searchSizeFilter.max > 0)) { for (const row of this.getRowValues()) { - if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms)) continue; if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms)) @@ -2027,7 +2045,7 @@ window.qBittorrent.DynamicTable ??= (() => { } const column = this.columns[this.sortedColumn]; - const isReverseSort = (this.reverseSort === "0"); + const isReverseSort = this.reverseSort === "0"; filteredRows.sort((row1, row2) => { const result = column.compareRows(row1, row2); return isReverseSort ? result : -result; @@ -2417,7 +2435,7 @@ window.qBittorrent.DynamicTable ??= (() => { while (stack.length > 0) { const node = stack.pop(); - this.#updateNodeVisibility(node, (shouldHide ? shouldHide : this.isCollapsed(node.root.rowId))); + this.#updateNodeVisibility(node, shouldHide ? shouldHide : this.isCollapsed(node.root.rowId)); stack.push(...node.children); } @@ -2549,7 +2567,6 @@ window.qBittorrent.DynamicTable ??= (() => { td.append(window.qBittorrent.TorrentContent.createDownloadCheckbox(id, fileId, value)); else window.qBittorrent.TorrentContent.updateDownloadCheckbox(downloadCheckbox, id, fileId, value); - }; this.columns["checked"].staticWidth = 50; @@ -2662,8 +2679,8 @@ window.qBittorrent.DynamicTable ??= (() => { } #sortNodesByColumn(root, column) { - const isColumnName = (column.name === this.fileNameColumn); - const isReverseSort = (this.reverseSort === "0"); + const isColumnName = column.name === this.fileNameColumn; + const isReverseSort = this.reverseSort === "0"; const stack = [root]; while (stack.length > 0) { @@ -2754,14 +2771,14 @@ window.qBittorrent.DynamicTable ??= (() => { return []; const hasRowsChanged = function(rowsString, prevRowsStringString) { - const rowsChanged = (rowsString !== prevRowsStringString); + const rowsChanged = rowsString !== prevRowsStringString; const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => { return (acc || (term !== this.prevFilterTerms[index])); }, false); - const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length) - || ((this.filterTerms.length > 0) && isFilterTermsChanged)); - const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn); - const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort); + const isFilterChanged = (this.filterTerms.length !== this.prevFilterTerms.length) + || ((this.filterTerms.length > 0) && isFilterTermsChanged); + const isSortedColumnChanged = this.prevSortedColumn !== this.sortedColumn; + const isReverseSortChanged = this.prevReverseSort !== this.reverseSort; return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged); }.bind(this); @@ -2842,10 +2859,10 @@ window.qBittorrent.DynamicTable ??= (() => { * Toggles the global checkbox and all checkboxes underneath */ toggleGlobalCheckbox() { - const checkbox = document.getElementById("rootMultiRename_cb"); + const checkbox = window.qBittorrent.Misc.getElementById("rootMultiRename_cb", "input"); const isChecked = checkbox.checked || checkbox.indeterminate; - for (const cb of document.querySelectorAll("input.RenamingCB")) { + for (const cb of (document.querySelectorAll("input.RenamingCB") as NodeListOf)) { cb.indeterminate = false; if (isChecked) { cb.checked = true; @@ -2867,7 +2884,7 @@ window.qBittorrent.DynamicTable ??= (() => { toggleNodeTreeCheckbox(rowId, checkState) { const node = this.getNode(rowId); node.checked = checkState; - const checkbox = document.getElementById(`cbRename${rowId}`); + const checkbox = document.getElementById(`cbRename${rowId}`) as BulkRenameFileCheckbox; checkbox.checked = node.checked === 0; checkbox.state = checkbox.checked ? "checked" : "unchecked"; @@ -2876,10 +2893,10 @@ window.qBittorrent.DynamicTable ??= (() => { } updateGlobalCheckbox() { - const checkbox = document.getElementById("rootMultiRename_cb"); + const checkbox = document.getElementById("rootMultiRename_cb") as BulkRenameFileCheckbox; const nodes = this.fileTree.toArray(); const isAllChecked = nodes.every((node) => node.checked === 0); - const isAllUnchecked = (() => nodes.every((node) => node.checked !== 0)); + const isAllUnchecked = () => nodes.every((node) => node.checked !== 0); if (isAllChecked) { checkbox.state = "checked"; checkbox.indeterminate = false; @@ -2958,7 +2975,7 @@ window.qBittorrent.DynamicTable ??= (() => { } checkbox.id = `cbRename${id}`; checkbox.dataset.id = id; - checkbox.checked = (node.checked === 0); + checkbox.checked = node.checked === 0; checkbox.state = checkbox.checked ? "checked" : "unchecked"; }; this.columns["checked"].staticWidth = 50; @@ -3007,7 +3024,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.append(span); } span.id = fileNameRenamedId; - span.textContent = node.renamed; + span.textContent = (node as any).renamed; }; for (const column of this.columns) { @@ -3165,8 +3182,8 @@ window.qBittorrent.DynamicTable ??= (() => { const img = document.createElement("img"); img.src = img_path; img.className = "stateIcon"; - img.width = "22"; - img.height = "22"; + img.width = 22; + img.height = 22; td.append(img); } } @@ -3191,7 +3208,7 @@ window.qBittorrent.DynamicTable ??= (() => { column["compareRows"] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); - if ((typeof(value1) === "number") && (typeof(value2) === "number")) + if ((typeof value1 === "number") && (typeof value2 === "number")) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; @@ -3243,8 +3260,8 @@ window.qBittorrent.DynamicTable ??= (() => { if (!tr) return; - const { name, torrentURL } = this._this.rows.get(this.rowId).full_data; - qBittorrent.Client.createAddTorrentWindow(name, torrentURL); + const { name, torrentURL } = this.rows.get(tr.rowId).full_data; + window.qBittorrent.Client.createAddTorrentWindow(name, torrentURL); }); } updateRow(tr, fullUpdate) { @@ -3273,7 +3290,7 @@ window.qBittorrent.DynamicTable ??= (() => { column["compareRows"] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); - if ((typeof(value1) === "number") && (typeof(value2) === "number")) + if ((typeof value1 === "number") && (typeof value2 === "number")) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; @@ -3306,7 +3323,7 @@ window.qBittorrent.DynamicTable ??= (() => { checkbox.addEventListener("click", function(e) { window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({ rowId: row.rowId, - checked: this.checked + checked: this.checked, }); window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked); e.stopPropagation(); @@ -3315,7 +3332,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.append(checkbox); } else { - document.getElementById(`cbRssDlRule${row.rowId}`).checked = row.full_data.checked; + window.qBittorrent.Misc.getElementById(`cbRssDlRule${row.rowId}`, "input").checked = row.full_data.checked; } }; this.columns["checked"].staticWidth = 50; @@ -3356,7 +3373,7 @@ window.qBittorrent.DynamicTable ??= (() => { column["compareRows"] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); - if ((typeof(value1) === "number") && (typeof(value2) === "number")) + if ((typeof value1 === "number") && (typeof value2 === "number")) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; @@ -3403,7 +3420,7 @@ window.qBittorrent.DynamicTable ??= (() => { checkbox.addEventListener("click", function(e) { window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({ rowId: row.rowId, - checked: this.checked + checked: this.checked, }); e.stopPropagation(); }); @@ -3411,7 +3428,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.append(checkbox); } else { - document.getElementById(`cbRssDlFeed${row.rowId}`).checked = row.full_data.checked; + window.qBittorrent.Misc.getElementById(`cbRssDlFeed${row.rowId}`, "input").checked = row.full_data.checked; } }; this.columns["checked"].staticWidth = 50; @@ -3441,7 +3458,7 @@ window.qBittorrent.DynamicTable ??= (() => { column["compareRows"] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); - if ((typeof(value1) === "number") && (typeof(value2) === "number")) + if ((typeof value1 === "number") && (typeof value2 === "number")) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; @@ -3489,7 +3506,7 @@ window.qBittorrent.DynamicTable ??= (() => { column["compareRows"] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); - if ((typeof(value1) === "number") && (typeof(value2) === "number")) + if ((typeof value1 === "number") && (typeof value2 === "number")) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; @@ -3517,6 +3534,7 @@ window.qBittorrent.DynamicTable ??= (() => { class LogMessageTable extends DynamicTable { filterText = ""; + filteredLength = 0; initColumns() { this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true); @@ -3585,7 +3603,7 @@ window.qBittorrent.DynamicTable ??= (() => { } const column = this.columns[this.sortedColumn]; - const isReverseSort = (this.reverseSort === "0"); + const isReverseSort = this.reverseSort === "0"; filteredRows.sort((row1, row2) => { const result = column.compareRows(row1, row2); return isReverseSort ? result : -result; @@ -3644,7 +3662,7 @@ window.qBittorrent.DynamicTable ??= (() => { } const column = this.columns[this.sortedColumn]; - const isReverseSort = (this.reverseSort === "0"); + const isReverseSort = this.reverseSort === "0"; filteredRows.sort((row1, row2) => { const result = column.compareRows(row1, row2); return isReverseSort ? result : -result; @@ -3816,9 +3834,7 @@ window.qBittorrent.DynamicTable ??= (() => { // private this.columns["private"].updateTd = function(td, row) { const isPrivate = this.getRowValue(row); - const string = isPrivate - ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" - : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]"; + const string = isPrivate ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]"; td.textContent = string; td.title = string; }; diff --git a/src/webui/www/private/scripts/file-tree.js b/src/webui/www/private/scripts/file-tree.ts similarity index 89% rename from src/webui/www/private/scripts/file-tree.js rename to src/webui/www/private/scripts/file-tree.ts index 96bc11aaeb5d..5ef77bd62c18 100644 --- a/src/webui/www/private/scripts/file-tree.js +++ b/src/webui/www/private/scripts/file-tree.ts @@ -26,10 +26,8 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; -window.qBittorrent.FileTree ??= (() => { +const fileTreeModule = (() => { const exports = () => { return { FilePriority: FilePriority, @@ -45,26 +43,26 @@ window.qBittorrent.FileTree ??= (() => { Normal: 1, High: 6, Maximum: 7, - Mixed: -1 + Mixed: -1, }; Object.freeze(FilePriority); const TriState = { Unchecked: 0, Checked: 1, - Partial: 2 + Partial: 2, }; Object.freeze(TriState); class FileTree { - #root = null; - #nodeMap = {}; // Object with Number as keys is faster than anything + #root: FileNode | FolderNode = null; + #nodeMap: Record = {}; // Object with Number as keys is faster than anything setRoot(root) { this.#root = root; this.#generateNodeMap(root); - if (this.#root.isFolder) + if (this.#root instanceof FolderNode) this.#root.calculateSize(); } @@ -113,8 +111,8 @@ window.qBittorrent.FileTree ??= (() => { class FileNode { name = ""; path = ""; - rowId = null; - fileId = null; + rowId: number = null; + fileId: number = null; size = 0; checked = TriState.Unchecked; remaining = 0; @@ -122,9 +120,9 @@ window.qBittorrent.FileTree ??= (() => { priority = FilePriority.Normal; availability = 0; depth = 0; - root = null; + root: FileNode | FolderNode = null; isFolder = false; - children = []; + children: (FileNode | FolderNode)[] = []; isIgnored() { return this.priority === FilePriority.Ignored; @@ -144,7 +142,7 @@ window.qBittorrent.FileTree ??= (() => { remaining: this.remaining, progress: this.progress, priority: this.priority, - availability: this.availability + availability: this.availability, }; } } @@ -166,13 +164,13 @@ window.qBittorrent.FileTree ??= (() => { * Calculate size of node and its children */ calculateSize() { - const stack = [this]; - const visited = []; + const stack: (FileNode | FolderNode)[] = [this]; + const visited: (FileNode | FolderNode)[] = []; while (stack.length > 0) { const root = stack.at(-1); - if (root.isFolder) { + if (root instanceof FolderNode) { if (visited.at(-1) !== root) { visited.push(root); stack.push(...root.children); @@ -207,8 +205,8 @@ window.qBittorrent.FileTree ??= (() => { if (!child.isIgnored()) { root.remaining += child.remaining; - root.progress += (child.progress * child.size); - root.availability += (child.availability * child.size); + root.progress += child.progress * child.size; + root.availability += child.availability * child.size; } } @@ -235,4 +233,6 @@ window.qBittorrent.FileTree ??= (() => { return exports(); })(); + +window.qBittorrent.FileTree ??= fileTreeModule; Object.freeze(window.qBittorrent.FileTree); diff --git a/src/webui/www/private/scripts/filesystem.js b/src/webui/www/private/scripts/filesystem.ts similarity index 98% rename from src/webui/www/private/scripts/filesystem.js rename to src/webui/www/private/scripts/filesystem.ts index 5ab831fe3e17..066b021207b9 100644 --- a/src/webui/www/private/scripts/filesystem.js +++ b/src/webui/www/private/scripts/filesystem.ts @@ -26,8 +26,6 @@ * exception statement from your version. */ -"use strict"; - // This file is the JavaScript implementation of base/utils/fs.cpp window.qBittorrent ??= {}; @@ -37,7 +35,7 @@ window.qBittorrent.Filesystem ??= (() => { PathSeparator: PathSeparator, fileExtension: fileExtension, fileName: fileName, - folderName: folderName + folderName: folderName, }; }; diff --git a/src/webui/www/private/scripts/jsconfig.json b/src/webui/www/private/scripts/jsconfig.json new file mode 100644 index 000000000000..f7ae5f93ed0a --- /dev/null +++ b/src/webui/www/private/scripts/jsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "checkJs": true, + "allowJs": true, + "noEmit": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "strict": false, + "noImplicitAny": false, + "strictNullChecks": false, + "strictFunctionTypes": false, + "strictBindCallApply": false, + "strictPropertyInitialization": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "typeRoots": ["./types"], + "paths": { + "*": ["*", "types/*"] + } + }, + "include": [ + "**/*.js", + "types/**/*.d.ts" + ], + "exclude": [ + "node_modules", + "lib/MooTools-*.js", + "lib/mocha*.js" + ] +} diff --git a/src/webui/www/private/scripts/localpreferences.js b/src/webui/www/private/scripts/localpreferences.ts similarity index 96% rename from src/webui/www/private/scripts/localpreferences.js rename to src/webui/www/private/scripts/localpreferences.ts index 865417038adc..90d768a8e239 100644 --- a/src/webui/www/private/scripts/localpreferences.js +++ b/src/webui/www/private/scripts/localpreferences.ts @@ -27,23 +27,19 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.LocalPreferences ??= (() => { const exports = () => { return { LocalPreferences: LocalPreferences, - upgrade: upgrade + upgrade: upgrade, }; }; class LocalPreferences { - get(key, defaultValue) { + get(key, defaultValue = undefined) { const value = localStorage.getItem(key); - return ((value === null) && (defaultValue !== undefined)) - ? defaultValue - : value; + return ((value === null) && (defaultValue !== undefined)) ? defaultValue : value; } set(key, value) { diff --git a/src/webui/www/private/scripts/misc.js b/src/webui/www/private/scripts/misc.ts similarity index 80% rename from src/webui/www/private/scripts/misc.js rename to src/webui/www/private/scripts/misc.ts index c1bad9e3e54a..6713d1297a6d 100644 --- a/src/webui/www/private/scripts/misc.js +++ b/src/webui/www/private/scripts/misc.ts @@ -26,10 +26,8 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; -window.qBittorrent.Misc ??= (() => { +const miscModule = (() => { const exports = () => { return { getHost: getHost, @@ -46,14 +44,15 @@ window.qBittorrent.Misc ??= (() => { containsAllTerms: containsAllTerms, sleep: sleep, downloadFile: downloadFile, + getElementById: getElementById, // variables FILTER_INPUT_DELAY: 400, - MAX_ETA: 8640000 + MAX_ETA: 8640000, }; }; // getHost emulate the GUI version `QString getHost(const QString &url)` - const getHost = (url) => { + const getHost = (url: string) => { // We want the hostname. // If failed to parse the domain, original input should be returned @@ -76,11 +75,11 @@ window.qBittorrent.Misc ??= (() => { } }; - const createDebounceHandler = (delay, func) => { + const createDebounceHandler = (delay: number, func: (...params) => void) => { let timer = -1; return (...params) => { clearTimeout(timer); - timer = setTimeout(() => { + timer = window.setTimeout(() => { func(...params); timer = -1; @@ -91,7 +90,7 @@ window.qBittorrent.Misc ??= (() => { /* * JS counterpart of the function in src/misc.cpp */ - const friendlyUnit = (value, isSpeed) => { + const friendlyUnit = (value: number | null | undefined, isSpeed?: boolean) => { if ((value === undefined) || (value === null) || Number.isNaN(value) || (value < 0)) return "QBT_TR(Unknown)QBT_TR[CONTEXT=misc]"; @@ -102,7 +101,7 @@ window.qBittorrent.Misc ??= (() => { "QBT_TR(GiB)QBT_TR[CONTEXT=misc]", "QBT_TR(TiB)QBT_TR[CONTEXT=misc]", "QBT_TR(PiB)QBT_TR[CONTEXT=misc]", - "QBT_TR(EiB)QBT_TR[CONTEXT=misc]" + "QBT_TR(EiB)QBT_TR[CONTEXT=misc]", ]; const friendlyUnitPrecision = (sizeUnit) => { @@ -110,7 +109,8 @@ window.qBittorrent.Misc ??= (() => { return 1; else if (sizeUnit === 3) // GiB return 2; - else // TiB, PiB, EiB + // TiB, PiB, EiB + else return 3; }; @@ -138,7 +138,7 @@ window.qBittorrent.Misc ??= (() => { /* * JS counterpart of the function in src/misc.cpp */ - const friendlyDuration = (seconds, maxCap = -1) => { + const friendlyDuration = (seconds: number, maxCap = -1) => { if ((seconds < 0) || ((seconds >= maxCap) && (maxCap >= 0))) return "∞"; if (seconds === 0) @@ -147,21 +147,21 @@ window.qBittorrent.Misc ??= (() => { return "QBT_TR(< 1m)QBT_TR[CONTEXT=misc]"; let minutes = seconds / 60; if (minutes < 60) - return "QBT_TR(%1m)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(minutes)); + return "QBT_TR(%1m)QBT_TR[CONTEXT=misc]".replace("%1", String(Math.floor(minutes))); let hours = minutes / 60; minutes %= 60; if (hours < 24) - return "QBT_TR(%1h %2m)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(hours)).replace("%2", Math.floor(minutes)); + return "QBT_TR(%1h %2m)QBT_TR[CONTEXT=misc]".replace("%1", String(Math.floor(hours))).replace("%2", String(Math.floor(minutes))); let days = hours / 24; hours %= 24; if (days < 365) - return "QBT_TR(%1d %2h)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(days)).replace("%2", Math.floor(hours)); + return "QBT_TR(%1d %2h)QBT_TR[CONTEXT=misc]".replace("%1", String(Math.floor(days))).replace("%2", String(Math.floor(hours))); const years = days / 365; days %= 365; - return "QBT_TR(%1y %2d)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(years)).replace("%2", Math.floor(days)); + return "QBT_TR(%1y %2d)QBT_TR[CONTEXT=misc]".replace("%1", String(Math.floor(years))).replace("%2", String(Math.floor(days))); }; - const friendlyPercentage = (value) => { + const friendlyPercentage = (value: number) => { let percentage = value * 100; if (Number.isNaN(percentage) || (percentage < 0)) percentage = 0; @@ -173,20 +173,20 @@ window.qBittorrent.Misc ??= (() => { /* * JS counterpart of the function in src/misc.cpp */ - const parseHtmlLinks = (text) => { - const exp = /(\b(https?|ftp|file):\/\/[-\w+&@#/%?=~|!:,.;]*[-\w+&@#/%=~|])/gi; + const parseHtmlLinks = (text: string) => { + const exp = /(\b(?:https?|ftp|file):\/\/[-\w+&@#/%?=~|!:,.;]*[-\w+&@#/%=~|])/gi; return text.replace(exp, "$1"); }; - const parseVersion = (versionString) => { + const parseVersion = (versionString: string) => { const failure = { - valid: false + valid: false, }; if (typeof versionString !== "string") return failure; - const tryToNumber = (str) => { + const tryToNumber = (str: string) => { const num = Number(str); return (Number.isNaN(num) ? str : num); }; @@ -197,7 +197,7 @@ window.qBittorrent.Misc ??= (() => { major: ver[0], minor: ver[1], fix: ver[2], - patch: ver[3] + patch: ver[3], }; }; @@ -212,7 +212,7 @@ window.qBittorrent.Misc ??= (() => { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#parameters const naturalSortCollator = new Intl.Collator(undefined, { numeric: true, usage: "sort" }); - const safeTrim = (value) => { + const safeTrim = (value: string) => { try { return value.trim(); } @@ -223,14 +223,16 @@ window.qBittorrent.Misc ??= (() => { } }; - const toFixedPointString = (number, digits) => { + const toFixedPointString = (number: number, digits: number) => { if (Number.isNaN(number)) return number.toString(); const sign = (number < 0) ? "-" : ""; // Do not round up `number` // Small floating point numbers are imprecise, thus process as a String - const tmp = Math.trunc(`${Math.abs(number)}e${digits}`).toString(); + /* beautify ignore:start */ + const tmp = Math.trunc(`${Math.abs(number)}e${digits}` as any).toString(); + /* beautify ignore:end */ if (digits <= 0) { return (tmp === "0") ? tmp : `${sign}${tmp}`; } @@ -249,7 +251,7 @@ window.qBittorrent.Misc ??= (() => { * @param {Array} terms terms to search for within the text * @returns {Boolean} true if all terms match the text, false otherwise */ - const containsAllTerms = (text, terms) => { + const containsAllTerms = (text: string, terms: string[]) => { const textToSearch = text.toLowerCase(); return terms.every((term) => { const isTermRequired = term.startsWith("+"); @@ -267,13 +269,13 @@ window.qBittorrent.Misc ??= (() => { }); }; - const sleep = (ms) => { + const sleep = (ms: number) => { return new Promise((resolve) => { setTimeout(resolve, ms); }); }; - const downloadFile = async (url, defaultFileName, errorMessage = "QBT_TR(Unable to download file)QBT_TR[CONTEXT=HttpServer]") => { + const downloadFile = async (url: string | URL, defaultFileName: string, errorMessage = "QBT_TR(Unable to download file)QBT_TR[CONTEXT=HttpServer]") => { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { @@ -287,7 +289,7 @@ window.qBittorrent.Misc ??= (() => { let fileName = defaultFileName; if (fileNameHeader.startsWith(fileNamePrefix)) { fileName = fileNameHeader.substring(fileNamePrefix.length); - if (fileName.startsWith("\"") && fileName.endsWith("\"")) + if (fileName.startsWith('"') && fileName.endsWith('"')) fileName = fileName.slice(1, -1); } @@ -302,6 +304,23 @@ window.qBittorrent.Misc ??= (() => { } }; + type ElementTypeMap = { + input: HTMLInputElement; + select: HTMLSelectElement; + image: HTMLImageElement; + button: HTMLButtonElement; + ul: HTMLUListElement; + li: HTMLLIElement; + frame: HTMLIFrameElement; + template: HTMLTemplateElement; + }; + + const getElementById = (id: string, type: T): ElementTypeMap[T] => { + return document.getElementById(id) as ElementTypeMap[T]; + }; + return exports(); })(); + +window.qBittorrent.Misc ??= miscModule; Object.freeze(window.qBittorrent.Misc); diff --git a/src/webui/www/private/scripts/mocha-init.js b/src/webui/www/private/scripts/mocha-init.ts similarity index 88% rename from src/webui/www/private/scripts/mocha-init.js rename to src/webui/www/private/scripts/mocha-init.ts index e3d1300c32e0..009418da8ec3 100644 --- a/src/webui/www/private/scripts/mocha-init.js +++ b/src/webui/www/private/scripts/mocha-init.ts @@ -36,7 +36,6 @@ it in the onContentLoaded function of the new window. ----------------------------------------------------------------- */ -"use strict"; window.qBittorrent ??= {}; window.qBittorrent.Dialog ??= (() => { @@ -44,7 +43,7 @@ window.qBittorrent.Dialog ??= (() => { return { baseModalOptions: baseModalOptions, limitWidthToViewport: limitWidthToViewport, - limitHeightToViewport: limitHeightToViewport + limitHeightToViewport: limitHeightToViewport, }; }; @@ -92,14 +91,14 @@ window.qBittorrent.Dialog ??= (() => { top: 15, right: 10, bottom: 15, - left: 5 + left: 5, }, resizable: true, width: limitWidthToViewport(480), onCloseComplete: () => { // make sure overlay is properly hidden upon modal closing document.getElementById("modalOverlay").style.display = "none"; - } + }, }); deepFreeze(baseModalOptions); @@ -108,21 +107,21 @@ window.qBittorrent.Dialog ??= (() => { })(); Object.freeze(window.qBittorrent.Dialog); -let saveWindowSize = () => {}; -let loadWindowWidth = () => {}; -let loadWindowHeight = () => {}; -let showDownloadPage = () => {}; +let saveWindowSize = (windowName, windowId = windowName) => {}; +let loadWindowWidth = (windowId, defaultValue, limitToViewportWidth = true) => {}; +let loadWindowHeight = (windowId, defaultValue, limitToViewportHeight = true) => {}; +let showDownloadPage = (urls = []) => {}; let globalUploadLimitFN = () => {}; let uploadLimitFN = () => {}; let shareRatioFN = () => {}; let toggleSequentialDownloadFN = () => {}; let toggleFirstLastPiecePrioFN = () => {}; -let setSuperSeedingFN = () => {}; +let setSuperSeedingFN = (val) => {}; let setForceStartFN = () => {}; let globalDownloadLimitFN = () => {}; let StatisticsLinkFN = () => {}; let downloadLimitFN = () => {}; -let deleteSelectedTorrentsFN = () => {}; +let deleteSelectedTorrentsFN = (forceDeleteFiles = false) => {}; let stopFN = () => {}; let startFN = () => {}; let autoTorrentManagementFN = () => {}; @@ -135,25 +134,25 @@ let startVisibleTorrentsFN = () => {}; let stopVisibleTorrentsFN = () => {}; let deleteVisibleTorrentsFN = () => {}; let torrentNewCategoryFN = () => {}; -let torrentSetCategoryFN = () => {}; +let torrentSetCategoryFN = (category) => {}; let createCategoryFN = () => {}; -let createSubcategoryFN = () => {}; -let editCategoryFN = () => {}; -let removeCategoryFN = () => {}; +let createSubcategoryFN = (category) => {}; +let editCategoryFN = (category) => {}; +let removeCategoryFN = (category) => {}; let deleteUnusedCategoriesFN = () => {}; let torrentAddTagsFN = () => {}; -let torrentSetTagsFN = () => {}; +let torrentSetTagsFN = (tag, isSet) => {}; let torrentRemoveAllTagsFN = () => {}; let createTagFN = () => {}; -let removeTagFN = () => {}; +let removeTagFN = (tag) => {}; let deleteUnusedTagsFN = () => {}; -let deleteTrackerFN = () => {}; +let deleteTrackerFN = (trackerHost) => {}; let copyNameFN = () => {}; let copyInfohashFN = (policy) => {}; let copyMagnetLinkFN = () => {}; let copyIdFN = () => {}; let copyCommentFN = () => {}; -let setQueuePositionFN = () => {}; +let setQueuePositionFN = (cmd) => {}; let exportTorrentFN = () => {}; const initializeWindows = () => { @@ -179,10 +178,10 @@ const initializeWindows = () => { }; const addClickEvent = (el, fn) => { - ["Link", "Button"].each((item) => { + for (const item of ["Link", "Button"]) { if (document.getElementById(el + item)) document.getElementById(el + item).addEventListener("click", fn); - }); + } }; addClickEvent("download", (e) => { @@ -191,15 +190,15 @@ const initializeWindows = () => { showDownloadPage(); }); - showDownloadPage = (urls) => { + showDownloadPage = (urls = []) => { const id = "downloadPage"; - const contentURL = new URL("download.html", window.location); + const contentURL = new URL("download.html", window.location.href); if (urls && (urls.length > 0)) { contentURL.search = new URLSearchParams({ v: "${CACHEID}", - urls: urls.map(encodeURIComponent).join("|") - }); + urls: urls.map(encodeURIComponent).join("|"), + }).toString(); } new MochaUI.Window({ @@ -217,7 +216,7 @@ const initializeWindows = () => { height: loadWindowHeight(id, 300), onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { saveWindowSize(id); - }) + }), }); updateMainData(); }; @@ -244,7 +243,7 @@ const initializeWindows = () => { }, onClose: () => { window.qBittorrent.TorrentCreator.unload(); - } + }, }); }); @@ -261,7 +260,7 @@ const initializeWindows = () => { toolbar: true, contentURL: "views/preferences.html?v=${CACHEID}", require: { - css: ["css/Tabs.css?v=${CACHEID}"] + css: ["css/Tabs.css?v=${CACHEID}"], }, toolbarURL: "views/preferencesToolbar.html?v=${CACHEID}", maximizable: false, @@ -272,7 +271,7 @@ const initializeWindows = () => { height: loadWindowHeight(id, 600), onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { saveWindowSize(id); - }) + }), }); }); @@ -293,7 +292,7 @@ const initializeWindows = () => { height: loadWindowHeight(id, 400), onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { saveWindowSize(id); - }) + }), }); }); @@ -305,9 +304,10 @@ const initializeWindows = () => { // make the entire anchor tag trigger the input, despite the input's label not spanning the entire anchor document.getElementById("uploadLink").addEventListener("click", (e) => { const fileSelector = document.getElementById("fileselectLink"); + const target = e.target as HTMLInputElement; // clear the value so that reselecting the same file(s) still triggers the 'change' event - if (e.target === fileSelector) { - e.target.value = null; + if (target === fileSelector) { + target.value = null; } else { e.preventDefault(); @@ -315,7 +315,7 @@ const initializeWindows = () => { } }); - for (const element of document.querySelectorAll("#uploadButton #fileselectButton, #uploadLink #fileselectLink")) { + for (const element of document.querySelectorAll("#uploadButton #fileselectButton, #uploadLink #fileselectLink") as NodeListOf) { element.addEventListener("change", (event) => { if (element.files.length === 0) return; @@ -325,12 +325,12 @@ const initializeWindows = () => { } globalUploadLimitFN = () => { - const contentURL = new URL("speedlimit.html", window.location); + const contentURL = new URL("speedlimit.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hashes: "global", type: "upload", - }); + }).toString(); new MochaUI.Window({ id: "uploadLimitPage", icon: "images/qbittorrent-tray.svg", @@ -343,7 +343,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(424), - height: 100 + height: 100, }); }; @@ -352,12 +352,12 @@ const initializeWindows = () => { if (hashes.length <= 0) return; - const contentURL = new URL("speedlimit.html", window.location); + const contentURL = new URL("speedlimit.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hashes: hashes.join("|"), type: "upload", - }); + }).toString(); new MochaUI.Window({ id: "uploadLimitPage", icon: "images/qbittorrent-tray.svg", @@ -370,7 +370,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(424), - height: 100 + height: 100, }); }; @@ -398,13 +398,13 @@ const initializeWindows = () => { } } - const contentURL = new URL("shareratio.html", window.location); + const contentURL = new URL("shareratio.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hashes: hashes.join("|"), // if all torrents have same share ratio, display that share ratio. else use the default - orig: torrentsHaveSameShareRatio ? shareRatio : "" - }); + orig: torrentsHaveSameShareRatio ? shareRatio : "", + }).toString(); new MochaUI.Window({ id: "shareRatioPage", icon: "images/qbittorrent-tray.svg", @@ -416,7 +416,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(500), - height: 250 + height: 250, }); }; @@ -426,8 +426,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/toggleSequentialDownload", { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); updateMainData(); } @@ -439,8 +439,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/toggleFirstLastPiecePrio", { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); updateMainData(); } @@ -453,8 +453,8 @@ const initializeWindows = () => { method: "POST", body: new URLSearchParams({ hashes: hashes.join("|"), - value: val - }) + value: val, + }), }); updateMainData(); } @@ -467,20 +467,20 @@ const initializeWindows = () => { method: "POST", body: new URLSearchParams({ hashes: hashes.join("|"), - value: "true" - }) + value: "true", + }), }); updateMainData(); } }; globalDownloadLimitFN = () => { - const contentURL = new URL("speedlimit.html", window.location); + const contentURL = new URL("speedlimit.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hashes: "global", type: "download", - }); + }).toString(); new MochaUI.Window({ id: "downloadLimitPage", icon: "images/qbittorrent-tray.svg", @@ -493,7 +493,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(424), - height: 100 + height: 100, }); }; @@ -514,7 +514,7 @@ const initializeWindows = () => { }), onContentLoaded: () => { window.qBittorrent.Statistics.render(); - } + }, }); }; @@ -523,12 +523,12 @@ const initializeWindows = () => { if (hashes.length <= 0) return; - const contentURL = new URL("speedlimit.html", window.location); + const contentURL = new URL("speedlimit.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hashes: hashes.join("|"), type: "download", - }); + }).toString(); new MochaUI.Window({ id: "downloadLimitPage", icon: "images/qbittorrent-tray.svg", @@ -541,7 +541,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(424), - height: 100 + height: 100, }); }; @@ -555,7 +555,7 @@ const initializeWindows = () => { title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]", data: { hashes: hashes, - forceDeleteFiles: forceDeleteFiles + forceDeleteFiles: forceDeleteFiles, }, contentURL: "views/confirmdeletion.html?v=${CACHEID}", onContentLoaded: (w) => { @@ -565,17 +565,17 @@ const initializeWindows = () => { onCloseComplete: () => { // make sure overlay is properly hidden upon modal closing document.getElementById("modalOverlay").style.display = "none"; - } + }, }); } else { fetch("api/v2/torrents/delete", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|"), - deleteFiles: forceDeleteFiles - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + deleteFiles: String(forceDeleteFiles), + }), + }) .then((response) => { if (!response.ok) { alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]"); @@ -602,8 +602,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/stop", { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); updateMainData(); } @@ -615,8 +615,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/start", { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); updateMainData(); } @@ -633,19 +633,19 @@ const initializeWindows = () => { title: "QBT_TR(Enable automatic torrent management)QBT_TR[CONTEXT=confirmAutoTMMDialog]", data: { hashes: hashes, - enable: enableAutoTMM + enable: enableAutoTMM, }, - contentURL: "views/confirmAutoTMM.html?v=${CACHEID}" + contentURL: "views/confirmAutoTMM.html?v=${CACHEID}", }); } else { fetch("api/v2/torrents/setAutoManagement", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|"), - enable: enableAutoTMM - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + enable: enableAutoTMM, + }), + }) .then((response) => { if (!response.ok) { alert("QBT_TR(Unable to set Auto Torrent Management for the selected torrents.)QBT_TR[CONTEXT=HttpServer]"); @@ -667,16 +667,16 @@ const initializeWindows = () => { id: "confirmRecheckDialog", title: "QBT_TR(Recheck confirmation)QBT_TR[CONTEXT=confirmRecheckDialog]", data: { hashes: hashes }, - contentURL: "views/confirmRecheck.html?v=${CACHEID}" + contentURL: "views/confirmRecheck.html?v=${CACHEID}", }); } else { fetch("api/v2/torrents/recheck", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|"), - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + }), + }) .then((response) => { if (!response.ok) { alert("QBT_TR(Unable to recheck torrents.)QBT_TR[CONTEXT=HttpServer]"); @@ -695,8 +695,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/reannounce", { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); updateMainData(); } @@ -707,12 +707,12 @@ const initializeWindows = () => { if (hashes.length <= 0) return; - const contentURL = new URL("setlocation.html", window.location); + const contentURL = new URL("setlocation.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hashes: hashes.join("|"), - path: encodeURIComponent(torrentsTable.getRow(hashes[0]).full_data.save_path) - }); + path: encodeURIComponent(torrentsTable.getRow(hashes[0]).full_data.save_path), + }).toString(); new MochaUI.Window({ id: "setLocationPage", icon: "images/qbittorrent-tray.svg", @@ -725,7 +725,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(400), - height: 130 + height: 130, }); }; @@ -738,12 +738,12 @@ const initializeWindows = () => { if (!row) return; - const contentURL = new URL("rename.html", window.location); + const contentURL = new URL("rename.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hash: hashes[0], - name: row.full_data.name - }); + name: row.full_data.name, + }).toString(); new MochaUI.Window({ id: "renamePage", icon: "images/qbittorrent-tray.svg", @@ -756,7 +756,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(400), - height: 100 + height: 100, }); }; @@ -780,7 +780,7 @@ const initializeWindows = () => { paddingHorizontal: 0, width: 800, height: 420, - resizeLimit: { x: [800], y: [420] } + resizeLimit: { x: [800], y: [420] }, }); } } @@ -790,11 +790,11 @@ const initializeWindows = () => { const hashes = torrentsTable.getFilteredTorrentsHashes(selectedStatus, selectedCategory, selectedTag, selectedTracker); if (hashes.length > 0) { fetch("api/v2/torrents/start", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|") - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + }), + }) .then((response) => { if (!response.ok) { alert("QBT_TR(Unable to start torrents.)QBT_TR[CONTEXT=HttpServer]"); @@ -811,11 +811,11 @@ const initializeWindows = () => { const hashes = torrentsTable.getFilteredTorrentsHashes(selectedStatus, selectedCategory, selectedTag, selectedTracker); if (hashes.length > 0) { fetch("api/v2/torrents/stop", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|") - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + }), + }) .then((response) => { if (!response.ok) { alert("QBT_TR(Unable to stop torrents.)QBT_TR[CONTEXT=HttpServer]"); @@ -838,23 +838,23 @@ const initializeWindows = () => { title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]", data: { hashes: hashes, - isDeletingVisibleTorrents: true + isDeletingVisibleTorrents: true, }, contentURL: "views/confirmdeletion.html?v=${CACHEID}", onContentLoaded: (w) => { MochaUI.resizeWindow(w, { centered: true }); MochaUI.centerWindow(w); - } + }, }); } else { fetch("api/v2/torrents/delete", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|"), - deleteFiles: false, - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + deleteFiles: "false", + }), + }) .then((response) => { if (!response.ok) { alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]"); @@ -874,12 +874,12 @@ const initializeWindows = () => { if (hashes.length <= 0) return; - const contentURL = new URL("newcategory.html", window.location); + const contentURL = new URL("newcategory.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", action: "set", - hashes: hashes.join("|") - }); + hashes: hashes.join("|"), + }).toString(); new MochaUI.Window({ id: "newCategoryPage", icon: "images/qbittorrent-tray.svg", @@ -892,7 +892,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(400), - height: 200 + height: 200, }); }; @@ -902,12 +902,12 @@ const initializeWindows = () => { return; fetch("api/v2/torrents/setCategory", { - method: "POST", - body: new URLSearchParams({ - hashes: hashes.join("|"), - category: category - }) - }) + method: "POST", + body: new URLSearchParams({ + hashes: hashes.join("|"), + category: category, + }), + }) .then((response) => { if (!response.ok) return; @@ -917,11 +917,11 @@ const initializeWindows = () => { }; createCategoryFN = () => { - const contentURL = new URL("newcategory.html", window.location); + const contentURL = new URL("newcategory.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", - action: "create" - }); + action: "create", + }).toString(); new MochaUI.Window({ id: "newCategoryPage", icon: "images/qbittorrent-tray.svg", @@ -934,17 +934,17 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(400), - height: 200 + height: 200, }); }; createSubcategoryFN = (category) => { - const contentURL = new URL("newcategory.html", window.location); + const contentURL = new URL("newcategory.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", action: "createSubcategory", - categoryName: `${category}/` - }); + categoryName: `${category}/`, + }).toString(); new MochaUI.Window({ id: "newSubcategoryPage", icon: "images/qbittorrent-tray.svg", @@ -957,17 +957,17 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(400), - height: 200 + height: 200, }); }; editCategoryFN = (category) => { - const contentURL = new URL("newcategory.html", window.location); + const contentURL = new URL("newcategory.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", action: "edit", - categoryName: category - }); + categoryName: category, + }).toString(); new MochaUI.Window({ id: "editCategoryPage", icon: "images/qbittorrent-tray.svg", @@ -980,17 +980,17 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(400), - height: 200 + height: 200, }); }; removeCategoryFN = (category) => { fetch("api/v2/torrents/removeCategories", { - method: "POST", - body: new URLSearchParams({ - categories: category - }) - }) + method: "POST", + body: new URLSearchParams({ + categories: category, + }), + }) .then((response) => { if (!response.ok) return; @@ -1007,11 +1007,11 @@ const initializeWindows = () => { categories.push(category); } fetch("api/v2/torrents/removeCategories", { - method: "POST", - body: new URLSearchParams({ - categories: categories.join("\n") - }) - }) + method: "POST", + body: new URLSearchParams({ + categories: categories.join("\n"), + }), + }) .then((response) => { if (!response.ok) return; @@ -1026,12 +1026,12 @@ const initializeWindows = () => { if (hashes.length <= 0) return; - const contentURL = new URL("newtag.html", window.location); + const contentURL = new URL("newtag.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", action: "set", - hashes: hashes.join("|") - }); + hashes: hashes.join("|"), + }).toString(); new MochaUI.Window({ id: "newTagPage", icon: "images/qbittorrent-tray.svg", @@ -1044,7 +1044,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(250), - height: 100 + height: 100, }); }; @@ -1053,12 +1053,12 @@ const initializeWindows = () => { if (hashes.length <= 0) return; - fetch((isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"), { + fetch(isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags", { method: "POST", body: new URLSearchParams({ hashes: hashes.join("|"), - tags: tag - }) + tags: tag, + }), }); }; @@ -1068,18 +1068,18 @@ const initializeWindows = () => { fetch("api/v2/torrents/removeTags", { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); } }; createTagFN = () => { - const contentURL = new URL("newtag.html", window.location); + const contentURL = new URL("newtag.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", - action: "create" - }); + action: "create", + }).toString(); new MochaUI.Window({ id: "newTagPage", icon: "images/qbittorrent-tray.svg", @@ -1092,7 +1092,7 @@ const initializeWindows = () => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(250), - height: 100 + height: 100, }); updateMainData(); }; @@ -1101,8 +1101,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/deleteTags", { method: "POST", body: new URLSearchParams({ - tags: tag - }) + tags: tag, + }), }); window.qBittorrent.Filters.clearTagFilter(); }; @@ -1116,26 +1116,28 @@ const initializeWindows = () => { fetch("api/v2/torrents/deleteTags", { method: "POST", body: new URLSearchParams({ - tags: tags.join(",") - }) + tags: tags.join(","), + }), }); window.qBittorrent.Filters.clearTagFilter(); }; deleteTrackerFN = (trackerHost) => { - if ((trackerHost === TRACKERS_ALL) + if ( + (trackerHost === TRACKERS_ALL) || (trackerHost === TRACKERS_ANNOUNCE_ERROR) || (trackerHost === TRACKERS_ERROR) || (trackerHost === TRACKERS_TRACKERLESS) - || (trackerHost === TRACKERS_WARNING)) + || (trackerHost === TRACKERS_WARNING) + ) return; - const contentURL = new URL("confirmtrackerdeletion.html", window.location); + const contentURL = new URL("confirmtrackerdeletion.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", host: trackerHost, - urls: [...trackerMap.get(trackerHost).keys()].map(encodeURIComponent).join("|") - }); + urls: [...trackerMap.get(trackerHost).keys()].map(encodeURIComponent).join("|"), + }).toString(); new MochaUI.Window({ id: "confirmDeletionPage", title: "QBT_TR(Remove tracker)QBT_TR[CONTEXT=confirmDeletionDlg]", @@ -1150,7 +1152,7 @@ const initializeWindows = () => { onCloseComplete: () => { updateMainData(); window.qBittorrent.Filters.clearTrackerFilter(); - } + }, }); }; @@ -1227,10 +1229,10 @@ const initializeWindows = () => { continue; const name = row.full_data.name; - const url = new URL("api/v2/torrents/export", window.location); + const url = new URL("api/v2/torrents/export", window.location.href); url.search = new URLSearchParams({ - hash: hash - }); + hash: hash, + }).toString(); // download response to file await window.qBittorrent.Misc.downloadFile(url, `${name}.torrent`, "QBT_TR(Unable to export torrent file)QBT_TR[CONTEXT=MainWindow]"); @@ -1248,8 +1250,8 @@ const initializeWindows = () => { fetch("api/v2/torrents/stop", { method: "POST", body: new URLSearchParams({ - hashes: "all" - }) + hashes: "all", + }), }); updateMainData(); } @@ -1263,14 +1265,14 @@ const initializeWindows = () => { fetch("api/v2/torrents/start", { method: "POST", body: new URLSearchParams({ - hashes: "all" - }) + hashes: "all", + }), }); updateMainData(); } }); - ["stop", "start", "recheck"].each((item) => { + for (const item of ["stop", "start", "recheck"]) { addClickEvent(item, (e) => { e.preventDefault(); e.stopPropagation(); @@ -1281,22 +1283,22 @@ const initializeWindows = () => { fetch(`api/v2/torrents/${item}`, { method: "POST", body: new URLSearchParams({ - hashes: hash - }) + hashes: hash, + }), }); }); updateMainData(); } }); - }); + } - ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item) => { + for (const item of ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"]) { addClickEvent(item, (e) => { e.preventDefault(); e.stopPropagation(); setQueuePositionFN(item); }); - }); + } setQueuePositionFN = (cmd) => { const hashes = torrentsTable.selectedRowsIds(); @@ -1304,8 +1306,8 @@ const initializeWindows = () => { fetch(`api/v2/torrents/${cmd}`, { method: "POST", body: new URLSearchParams({ - hashes: hashes.join("|") - }) + hashes: hashes.join("|"), + }), }); updateMainData(); } @@ -1323,7 +1325,7 @@ const initializeWindows = () => { loadMethod: "xhr", contentURL: "views/about.html?v=${CACHEID}", require: { - css: ["css/Tabs.css?v=${CACHEID}"] + css: ["css/Tabs.css?v=${CACHEID}"], }, toolbar: true, toolbarURL: "views/aboutToolbar.html?v=${CACHEID}", @@ -1332,7 +1334,7 @@ const initializeWindows = () => { height: loadWindowHeight(id, 360), onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { saveWindowSize(id); - }) + }), }); }); @@ -1341,13 +1343,13 @@ const initializeWindows = () => { e.stopPropagation(); fetch("api/v2/auth/logout", { - method: "POST" - }) + method: "POST", + }) .then((response) => { if (!response.ok) return; - window.location.reload(true); + (window.location as any).reload(true); }); }); @@ -1357,8 +1359,8 @@ const initializeWindows = () => { if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) { fetch("api/v2/app/shutdown", { - method: "POST" - }) + method: "POST", + }) .then((response) => { if (!response.ok) return; @@ -1380,9 +1382,9 @@ const initializeWindows = () => { }); } - const userAgent = (navigator.userAgentData?.platform ?? navigator.platform).toLowerCase(); + const userAgent = ((navigator as any).userAgentData?.platform ?? navigator.platform).toLowerCase(); if (userAgent.includes("ipad") || userAgent.includes("iphone") || (userAgent.includes("mac") && (navigator.maxTouchPoints > 1))) { - for (const element of document.getElementsByClassName("fileselect")) + for (const element of document.getElementsByClassName("fileselect") as HTMLCollectionOf) element.accept = ".torrent"; } }; diff --git a/src/webui/www/private/scripts/monkeypatch.js b/src/webui/www/private/scripts/monkeypatch.ts similarity index 93% rename from src/webui/www/private/scripts/monkeypatch.js rename to src/webui/www/private/scripts/monkeypatch.ts index 08399be9d91b..832ca860f3af 100644 --- a/src/webui/www/private/scripts/monkeypatch.js +++ b/src/webui/www/private/scripts/monkeypatch.ts @@ -27,13 +27,11 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.MonkeyPatch ??= (() => { const exports = () => { return { - patch: patch + patch: patch, }; }; @@ -48,10 +46,10 @@ window.qBittorrent.MonkeyPatch ??= (() => { // from allocating a `uniqueNumber` for elements that don't need it. // MooTools and MochaUI use it internally. - if (document.id === undefined) + if ((document as any).id === undefined) return; - document.id = (el) => { + (document as any).id = (el) => { if ((el === null) || (el === undefined)) return null; @@ -77,27 +75,27 @@ window.qBittorrent.MonkeyPatch ??= (() => { onload(); return true; } - // If the asset is loading, wait until it is loaded and then fire the onload function. // If asset doesn't load by a number of tries, fire onload anyway. else if (MUI.files[source] === "loading") { let tries = 0; - const checker = (function() { + let interval = -1; + const checker = function() { ++tries; - if ((MUI.files[source] === "loading") && (tries < "100")) + if ((MUI.files[source] === "loading") && (tries < 100)) return; - $clear(checker); + window.clearInterval(interval); if (typeof onload === "function") onload(); - }).periodical(50); + }; + interval = window.setInterval(checker, 50); } - // If the asset is not yet loaded or loading, start loading the asset. else { MUI.files[source] = "loading"; const properties = { - onload: (onload !== "undefined") ? onload : $empty + onload: (onload !== "undefined") ? onload : () => {}, }; // Add to the onload function diff --git a/src/webui/www/private/scripts/pathAutofill.js b/src/webui/www/private/scripts/pathAutofill.ts similarity index 87% rename from src/webui/www/private/scripts/pathAutofill.js rename to src/webui/www/private/scripts/pathAutofill.ts index 574120251d78..ba29d99706e7 100644 --- a/src/webui/www/private/scripts/pathAutofill.js +++ b/src/webui/www/private/scripts/pathAutofill.ts @@ -26,8 +26,6 @@ * exception statement from your version. */ -"use strict"; - /* File implementing auto-fill for the path input field in the path dialogs. */ @@ -36,7 +34,7 @@ window.qBittorrent ??= {}; window.qBittorrent.pathAutofill ??= (() => { const exports = () => { return { - attachPathAutofill: attachPathAutofill + attachPathAutofill: attachPathAutofill, }; }; @@ -65,24 +63,30 @@ window.qBittorrent.pathAutofill ??= (() => { return; fetch(`api/v2/app/getDirectoryContent?dirPath=${partialPath}&mode=${mode}`, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(response => response.json()) - .then(filesList => { showInputSuggestions(element, filesList); }) + .then(filesList => { + showInputSuggestions(element, filesList); + }) .catch(error => {}); }; function attachPathAutofill() { const directoryInputs = document.querySelectorAll(".pathDirectory:not(.pathAutoFillInitialized)"); for (const input of directoryInputs) { - input.addEventListener("input", function(event) { showPathSuggestions(this, "dirs"); }); + input.addEventListener("input", function(event) { + showPathSuggestions(this, "dirs"); + }); input.classList.add("pathAutoFillInitialized"); } const fileInputs = document.querySelectorAll(".pathFile:not(.pathAutoFillInitialized)"); for (const input of fileInputs) { - input.addEventListener("input", function(event) { showPathSuggestions(this, "all"); }); + input.addEventListener("input", function(event) { + showPathSuggestions(this, "all"); + }); input.classList.add("pathAutoFillInitialized"); } } diff --git a/src/webui/www/private/scripts/piecesbar.js b/src/webui/www/private/scripts/piecesbar.ts similarity index 94% rename from src/webui/www/private/scripts/piecesbar.js rename to src/webui/www/private/scripts/piecesbar.ts index 07bee0424987..0225dca25fa5 100644 --- a/src/webui/www/private/scripts/piecesbar.js +++ b/src/webui/www/private/scripts/piecesbar.ts @@ -26,13 +26,11 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.PiecesBar ??= (() => { const exports = () => { return { - PiecesBar: PiecesBar + PiecesBar: PiecesBar, }; }; @@ -61,7 +59,7 @@ window.qBittorrent.PiecesBar ??= (() => { haveColor: "hsl(210deg 55% 55%)", // @TODO palette vars not supported for this value, apply average borderSize: 1, borderColor: "var(--color-border-default)", - ...styles + ...styles, }; this.#canvasEl = document.createElement("canvas"); @@ -71,10 +69,11 @@ window.qBittorrent.PiecesBar ??= (() => { this.#ctx = this.#canvasEl.getContext("2d"); this.attachShadow({ mode: "open" }); - this.shadowRoot.host.id = `piecesbar_${this.#id}`; - this.shadowRoot.host.style.display = "block"; - this.shadowRoot.host.style.height = `${this.#styles.height}px`; - this.shadowRoot.host.style.border = `${this.#styles.borderSize}px solid ${this.#styles.borderColor}`; + const host = this.shadowRoot.host as HTMLElement; + host.id = `piecesbar_${this.#id}`; + host.style.display = "block"; + host.style.height = `${this.#styles.height}px`; + host.style.border = `${this.#styles.borderSize}px solid ${this.#styles.borderColor}`; this.shadowRoot.append(this.#canvasEl); this.#resizeObserver = new ResizeObserver(window.qBittorrent.Misc.createDebounceHandler(100, () => { @@ -171,7 +170,7 @@ window.qBittorrent.PiecesBar ??= (() => { const statusValues = { [PiecesBar.#STATUS_DOWNLOADING]: 0, - [PiecesBar.#STATUS_DOWNLOADED]: 0 + [PiecesBar.#STATUS_DOWNLOADED]: 0, }; // aggregate the status of each piece that contributes to this pixel @@ -204,8 +203,10 @@ window.qBittorrent.PiecesBar ??= (() => { lastValue = statusValues; // group contiguous colors together and draw as a single rectangle - if ((lastValue[PiecesBar.#STATUS_DOWNLOADING] === statusValues[PiecesBar.#STATUS_DOWNLOADING]) - && (lastValue[PiecesBar.#STATUS_DOWNLOADED] === statusValues[PiecesBar.#STATUS_DOWNLOADED])) + if ( + (lastValue[PiecesBar.#STATUS_DOWNLOADING] === statusValues[PiecesBar.#STATUS_DOWNLOADING]) + && (lastValue[PiecesBar.#STATUS_DOWNLOADED] === statusValues[PiecesBar.#STATUS_DOWNLOADED]) + ) continue; const rectangleWidth = x - rectangleStart; diff --git a/src/webui/www/private/scripts/progressbar.js b/src/webui/www/private/scripts/progressbar.ts similarity index 89% rename from src/webui/www/private/scripts/progressbar.js rename to src/webui/www/private/scripts/progressbar.ts index 90844e964f55..5accd2a86262 100644 --- a/src/webui/www/private/scripts/progressbar.js +++ b/src/webui/www/private/scripts/progressbar.ts @@ -26,13 +26,11 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.ProgressBar ??= (() => { const exports = () => { return { - ProgressBar: ProgressBar + ProgressBar: ProgressBar, }; }; @@ -79,13 +77,14 @@ window.qBittorrent.ProgressBar ??= (() => { this.#light.style.lineHeight = `${ProgressBar.#styles.height}px`; this.attachShadow({ mode: "open" }); - this.shadowRoot.host.id = this.#id; - this.shadowRoot.host.style.display = "block"; - this.shadowRoot.host.style.border = "1px solid var(--color-border-default)"; - this.shadowRoot.host.style.boxSizing = "content-box"; - this.shadowRoot.host.style.height = `${ProgressBar.#styles.height}px`; - this.shadowRoot.host.style.position = "relative"; - this.shadowRoot.host.style.margin = "0 auto"; + const host = this.shadowRoot.host as HTMLElement; + host.id = String(this.#id); + host.style.display = "block"; + host.style.border = "1px solid var(--color-border-default)"; + host.style.boxSizing = "content-box"; + host.style.height = `${ProgressBar.#styles.height}px`; + host.style.position = "relative"; + host.style.margin = "0 auto"; this.shadowRoot.appendChild(this.#dark); this.shadowRoot.appendChild(this.#light); diff --git a/src/webui/www/private/scripts/prop-files.js b/src/webui/www/private/scripts/prop-files.ts similarity index 89% rename from src/webui/www/private/scripts/prop-files.js rename to src/webui/www/private/scripts/prop-files.ts index f400761000c1..df87e4d057fd 100644 --- a/src/webui/www/private/scripts/prop-files.js +++ b/src/webui/www/private/scripts/prop-files.ts @@ -26,14 +26,12 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.PropFiles ??= (() => { const exports = () => { return { updateData: updateData, - clear: clear + clear: clear, }; }; @@ -47,18 +45,18 @@ window.qBittorrent.PropFiles ??= (() => { loadTorrentFilesDataTimer = -1; fetch("api/v2/torrents/filePrio", { - method: "POST", - body: new URLSearchParams({ - hash: current_hash, - id: fileIds.join("|"), - priority: priority - }) - }) + method: "POST", + body: new URLSearchParams({ + hash: current_hash, + id: fileIds.join("|"), + priority: priority, + }), + }) .then((response) => { if (!response.ok) return; - loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000); + loadTorrentFilesDataTimer = window.setTimeout(loadTorrentFilesData, 1000); }); }; @@ -66,8 +64,10 @@ window.qBittorrent.PropFiles ??= (() => { const loadTorrentFilesData = () => { if (document.hidden) return; - if (document.getElementById("propFiles").classList.contains("invisible") - || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { + if ( + document.getElementById("propFiles").classList.contains("invisible") + || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand") + ) { // Tab changed, don't do anything return; } @@ -84,14 +84,14 @@ window.qBittorrent.PropFiles ??= (() => { loadedNewTorrent = true; } - const url = new URL("api/v2/torrents/files", window.location); + const url = new URL("api/v2/torrents/files", window.location.href); url.search = new URLSearchParams({ - hash: current_hash - }); + hash: current_hash, + }).toString(); fetch(url, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -111,7 +111,7 @@ window.qBittorrent.PropFiles ??= (() => { }) .finally(() => { clearTimeout(loadTorrentFilesDataTimer); - loadTorrentFilesDataTimer = loadTorrentFilesData.delay(5000); + loadTorrentFilesDataTimer = window.setTimeout(loadTorrentFilesData, 1000); }); }; @@ -139,7 +139,7 @@ window.qBittorrent.PropFiles ??= (() => { height: 100, onCloseComplete: () => { updateData(); - } + }, }); }; @@ -161,7 +161,7 @@ window.qBittorrent.PropFiles ??= (() => { resizeLimit: { x: [800], y: [420] }, onCloseComplete: () => { updateData(); - } + }, }); }; diff --git a/src/webui/www/private/scripts/prop-general.js b/src/webui/www/private/scripts/prop-general.ts similarity index 80% rename from src/webui/www/private/scripts/prop-general.js rename to src/webui/www/private/scripts/prop-general.ts index 55d154cf708c..ebfdd089ac3f 100644 --- a/src/webui/www/private/scripts/prop-general.js +++ b/src/webui/www/private/scripts/prop-general.ts @@ -26,19 +26,17 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.PropGeneral ??= (() => { const exports = () => { return { updateData: updateData, - clear: clear + clear: clear, }; }; const piecesBar = new window.qBittorrent.PiecesBar.PiecesBar([], { - height: 18 + height: 18, }); document.getElementById("progress").appendChild(piecesBar); @@ -74,12 +72,14 @@ window.qBittorrent.PropGeneral ??= (() => { piecesBar.clear(); }; - let loadTorrentDataTimer = -1; + let loadTorrentDataTimer: number = -1; const loadTorrentData = () => { if (document.hidden) return; - if (document.getElementById("propGeneral").classList.contains("invisible") - || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { + if ( + document.getElementById("propGeneral").classList.contains("invisible") + || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand") + ) { // Tab changed, don't do anything return; } @@ -90,19 +90,19 @@ window.qBittorrent.PropGeneral ??= (() => { return; } - const propertiesURL = new URL("api/v2/torrents/properties", window.location); + const propertiesURL = new URL("api/v2/torrents/properties", window.location.href); propertiesURL.search = new URLSearchParams({ - hash: current_id - }); + hash: current_id, + }).toString(); fetch(propertiesURL, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) { document.getElementById("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; clearTimeout(loadTorrentDataTimer); - loadTorrentDataTimer = loadTorrentData.delay(10000); + loadTorrentDataTimer = window.setTimeout(loadTorrentData, 10000); return; } @@ -116,8 +116,8 @@ window.qBittorrent.PropGeneral ??= (() => { const timeElapsed = (data.seeding_time > 0) ? "QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=PropertiesWidget]" - .replace("%1", window.qBittorrent.Misc.friendlyDuration(data.time_elapsed)) - .replace("%2", window.qBittorrent.Misc.friendlyDuration(data.seeding_time)) + .replace("%1", window.qBittorrent.Misc.friendlyDuration(data.time_elapsed)) + .replace("%2", window.qBittorrent.Misc.friendlyDuration(data.seeding_time)) : window.qBittorrent.Misc.friendlyDuration(data.time_elapsed); document.getElementById("time_elapsed").textContent = timeElapsed; @@ -125,7 +125,7 @@ window.qBittorrent.PropGeneral ??= (() => { const nbConnections = "QBT_TR(%1 (%2 max))QBT_TR[CONTEXT=PropertiesWidget]" .replace("%1", data.nb_connections) - .replace("%2", ((data.nb_connections_limit < 0) ? "∞" : data.nb_connections_limit)); + .replace("%2", (data.nb_connections_limit < 0) ? "∞" : data.nb_connections_limit); document.getElementById("nb_connections").textContent = nbConnections; const totalDownloaded = "QBT_TR(%1 (%2 this session))QBT_TR[CONTEXT=PropertiesWidget]" @@ -148,14 +148,10 @@ window.qBittorrent.PropGeneral ??= (() => { .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.up_speed_avg, true)); document.getElementById("up_speed").textContent = upSpeed; - const dlLimit = (data.dl_limit === -1) - ? "∞" - : window.qBittorrent.Misc.friendlyUnit(data.dl_limit, true); + const dlLimit = (data.dl_limit === -1) ? "∞" : window.qBittorrent.Misc.friendlyUnit(data.dl_limit, true); document.getElementById("dl_limit").textContent = dlLimit; - const upLimit = (data.up_limit === -1) - ? "∞" - : window.qBittorrent.Misc.friendlyUnit(data.up_limit, true); + const upLimit = (data.up_limit === -1) ? "∞" : window.qBittorrent.Misc.friendlyUnit(data.up_limit, true); document.getElementById("up_limit").textContent = upLimit; document.getElementById("total_wasted").textContent = window.qBittorrent.Misc.friendlyUnit(data.total_wasted); @@ -176,9 +172,7 @@ window.qBittorrent.PropGeneral ??= (() => { document.getElementById("reannounce").textContent = window.qBittorrent.Misc.friendlyDuration(data.reannounce); - const lastSeen = (data.last_seen >= 0) - ? new Date(data.last_seen * 1000).toLocaleString() - : "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]"; + const lastSeen = (data.last_seen >= 0) ? new Date(data.last_seen * 1000).toLocaleString() : "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]"; document.getElementById("last_seen").textContent = lastSeen; const totalSize = (data.total_size >= 0) ? window.qBittorrent.Misc.friendlyUnit(data.total_size) : ""; @@ -186,75 +180,61 @@ window.qBittorrent.PropGeneral ??= (() => { const pieces = (data.pieces_num >= 0) ? "QBT_TR(%1 x %2 (have %3))QBT_TR[CONTEXT=PropertiesWidget]" - .replace("%1", data.pieces_num) - .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.piece_size)) - .replace("%3", data.pieces_have) + .replace("%1", data.pieces_num) + .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.piece_size)) + .replace("%3", data.pieces_have) : ""; document.getElementById("pieces").textContent = pieces; document.getElementById("created_by").textContent = data.created_by; - const additionDate = (data.addition_date >= 0) - ? new Date(data.addition_date * 1000).toLocaleString() - : "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; + const additionDate = (data.addition_date >= 0) ? new Date(data.addition_date * 1000).toLocaleString() : "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; document.getElementById("addition_date").textContent = additionDate; - const completionDate = (data.completion_date >= 0) - ? new Date(data.completion_date * 1000).toLocaleString() - : ""; + const completionDate = (data.completion_date >= 0) ? new Date(data.completion_date * 1000).toLocaleString() : ""; document.getElementById("completion_date").textContent = completionDate; - const creationDate = (data.creation_date >= 0) - ? new Date(data.creation_date * 1000).toLocaleString() - : ""; + const creationDate = (data.creation_date >= 0) ? new Date(data.creation_date * 1000).toLocaleString() : ""; document.getElementById("creation_date").textContent = creationDate; - const torrentHashV1 = (data.infohash_v1 !== "") - ? data.infohash_v1 - : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; + const torrentHashV1 = (data.infohash_v1 !== "") ? data.infohash_v1 : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; document.getElementById("torrent_hash_v1").textContent = torrentHashV1; - const torrentHashV2 = (data.infohash_v2 !== "") - ? data.infohash_v2 - : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; + const torrentHashV2 = (data.infohash_v2 !== "") ? data.infohash_v2 : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; document.getElementById("torrent_hash_v2").textContent = torrentHashV2; document.getElementById("save_path").textContent = data.save_path; document.getElementById("comment").innerHTML = window.qBittorrent.Misc.parseHtmlLinks(window.qBittorrent.Misc.escapeHtml(data.comment)); - document.getElementById("private").textContent = (data.has_metadata - ? (data.private - ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" - : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]") - : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"); + document.getElementById("private").textContent = data.has_metadata ? (data.private ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]") : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; } else { clearData(); } clearTimeout(loadTorrentDataTimer); - loadTorrentDataTimer = loadTorrentData.delay(5000); + loadTorrentDataTimer = window.setTimeout(loadTorrentData, 5000); }, (error) => { console.error(error); document.getElementById("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; clearTimeout(loadTorrentDataTimer); - loadTorrentDataTimer = loadTorrentData.delay(10000); + loadTorrentDataTimer = window.setTimeout(loadTorrentData, 10000); }); - const pieceStatesURL = new URL("api/v2/torrents/pieceStates", window.location); + const pieceStatesURL = new URL("api/v2/torrents/pieceStates", window.location.href); pieceStatesURL.search = new URLSearchParams({ - hash: current_id - }); + hash: current_id, + }).toString(); fetch(pieceStatesURL, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) { document.getElementById("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; clearTimeout(loadTorrentDataTimer); - loadTorrentDataTimer = loadTorrentData.delay(10000); + loadTorrentDataTimer = window.setTimeout(loadTorrentData, 10000); return; } @@ -267,13 +247,13 @@ window.qBittorrent.PropGeneral ??= (() => { clearData(); clearTimeout(loadTorrentDataTimer); - loadTorrentDataTimer = loadTorrentData.delay(5000); + loadTorrentDataTimer = window.setTimeout(loadTorrentData, 5000); }, (error) => { console.error(error); document.getElementById("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; clearTimeout(loadTorrentDataTimer); - loadTorrentDataTimer = loadTorrentData.delay(10000); + loadTorrentDataTimer = window.setTimeout(loadTorrentData, 10000); }); }; diff --git a/src/webui/www/private/scripts/prop-peers.js b/src/webui/www/private/scripts/prop-peers.ts similarity index 91% rename from src/webui/www/private/scripts/prop-peers.js rename to src/webui/www/private/scripts/prop-peers.ts index 9c4061897048..eb7e19d7414f 100644 --- a/src/webui/www/private/scripts/prop-peers.js +++ b/src/webui/www/private/scripts/prop-peers.ts @@ -26,14 +26,12 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.PropPeers ??= (() => { const exports = () => { return { updateData: updateData, - clear: clear + clear: clear, }; }; @@ -45,8 +43,10 @@ window.qBittorrent.PropPeers ??= (() => { const loadTorrentPeersData = () => { if (document.hidden) return; - if (document.getElementById("propPeers").classList.contains("invisible") - || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { + if ( + document.getElementById("propPeers").classList.contains("invisible") + || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand") + ) { syncTorrentPeersLastResponseId = 0; torrentPeersTable.clear(); return; @@ -58,15 +58,15 @@ window.qBittorrent.PropPeers ??= (() => { clearTimeout(loadTorrentPeersTimer); return; } - const url = new URL("api/v2/sync/torrentPeers", window.location); + const url = new URL("api/v2/sync/torrentPeers", window.location.href); url.search = new URLSearchParams({ hash: current_hash, - rid: syncTorrentPeersLastResponseId, - }); + rid: String(syncTorrentPeersLastResponseId), + }).toString(); fetch(url, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -75,7 +75,7 @@ window.qBittorrent.PropPeers ??= (() => { document.getElementById("error_div").textContent = ""; if (responseJSON) { - const full_update = (responseJSON["full_update"] === true); + const full_update = responseJSON["full_update"] === true; if (full_update) torrentPeersTable.clear(); if (responseJSON["rid"]) @@ -113,11 +113,10 @@ window.qBittorrent.PropPeers ??= (() => { else { torrentPeersTable.clear(); } - }) .finally(() => { clearTimeout(loadTorrentPeersTimer); - loadTorrentPeersTimer = loadTorrentPeersData.delay(window.qBittorrent.Client.getSyncMainDataInterval()); + loadTorrentPeersTimer = window.setTimeout(loadTorrentPeersData, window.qBittorrent.Client.getSyncMainDataInterval()); }); }; @@ -152,7 +151,7 @@ window.qBittorrent.PropPeers ??= (() => { paddingVertical: 0, paddingHorizontal: 0, width: window.qBittorrent.Dialog.limitWidthToViewport(350), - height: 260 + height: 260, }); }, banPeer: (element, ref) => { @@ -165,15 +164,15 @@ window.qBittorrent.PropPeers ??= (() => { method: "POST", body: new URLSearchParams({ hash: torrentsTable.getCurrentTorrentID(), - peers: selectedPeers.join("|") - }) + peers: selectedPeers.join("|"), + }), }); } - } + }, }, offsets: { x: 0, - y: 2 + y: 2, }, onShow: function() { const selectedPeers = torrentPeersTable.selectedRowsIds(); @@ -186,7 +185,7 @@ window.qBittorrent.PropPeers ??= (() => { this.hideItem("copyPeer"); this.hideItem("banPeer"); } - } + }, }); document.getElementById("CopyPeerInfo").addEventListener("click", async (event) => { diff --git a/src/webui/www/private/scripts/prop-trackers.js b/src/webui/www/private/scripts/prop-trackers.ts similarity index 93% rename from src/webui/www/private/scripts/prop-trackers.js rename to src/webui/www/private/scripts/prop-trackers.ts index e25f2e3b7d9e..0eb18bcea9e5 100644 --- a/src/webui/www/private/scripts/prop-trackers.js +++ b/src/webui/www/private/scripts/prop-trackers.ts @@ -26,15 +26,13 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.PropTrackers ??= (() => { const exports = () => { return { editTracker: editTrackerFN, updateData: updateData, - clear: clear + clear: clear, }; }; @@ -65,8 +63,10 @@ window.qBittorrent.PropTrackers ??= (() => { const loadTrackersData = () => { if (document.hidden) return; - if (document.getElementById("propTrackers").classList.contains("invisible") - || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { + if ( + document.getElementById("propTrackers").classList.contains("invisible") + || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand") + ) { // Tab changed, don't do anything return; } @@ -83,14 +83,14 @@ window.qBittorrent.PropTrackers ??= (() => { current_hash = new_hash; } - const url = new URL("api/v2/torrents/trackers", window.location); + const url = new URL("api/v2/torrents/trackers", window.location.href); url.search = new URLSearchParams({ - hash: current_hash - }); + hash: current_hash, + }).toString(); fetch(url, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -118,7 +118,7 @@ window.qBittorrent.PropTrackers ??= (() => { minAnnounce: tracker.min_announce, _isTracker: true, _hasEndpoints: tracker.endpoints && (tracker.endpoints.length > 0), - _sortable: !tracker.url.startsWith("** [") + _sortable: !tracker.url.startsWith("** ["), }; torrentTrackersTable.updateRowData(row); @@ -155,7 +155,7 @@ window.qBittorrent.PropTrackers ??= (() => { }) .finally(() => { clearTimeout(loadTrackersDataTimer); - loadTrackersDataTimer = loadTrackersData.delay(window.qBittorrent.Client.getSyncMainDataInterval()); + loadTrackersDataTimer = window.setTimeout(loadTrackersData, window.qBittorrent.Client.getSyncMainDataInterval()); }); }; @@ -183,11 +183,11 @@ window.qBittorrent.PropTrackers ??= (() => { }, ReannounceAllTrackers: (element, ref) => { reannounceTrackersFN(element, []); - } + }, }, offsets: { x: 0, - y: 2 + y: 2, }, onShow: function() { const selectedTrackers = torrentTrackersTable.selectedRowsIds(); @@ -220,7 +220,7 @@ window.qBittorrent.PropTrackers ??= (() => { this.showItem("ReannounceAllTrackers"); } } - } + }, }); const addTrackerFN = () => { @@ -247,7 +247,7 @@ window.qBittorrent.PropTrackers ??= (() => { height: 260, onCloseComplete: () => { updateData(); - } + }, }); }; @@ -256,13 +256,13 @@ window.qBittorrent.PropTrackers ??= (() => { return; const tracker = torrentTrackersTable.getRow(torrentTrackersTable.getSelectedRowId()); - const contentURL = new URL("edittracker.html", window.location); + const contentURL = new URL("edittracker.html", window.location.href); contentURL.search = new URLSearchParams({ v: "${CACHEID}", hash: current_hash, url: tracker.full_data.url, - tier: tracker.full_data.tier - }); + tier: tracker.full_data.tier, + }).toString(); new MochaUI.Window({ id: "trackersPage", @@ -280,7 +280,7 @@ window.qBittorrent.PropTrackers ??= (() => { height: 200, onCloseComplete: () => { updateData(); - } + }, }); }; @@ -293,12 +293,12 @@ window.qBittorrent.PropTrackers ??= (() => { current_hash = selectedTorrents.map(encodeURIComponent).join("|"); fetch("api/v2/torrents/removeTrackers", { - method: "POST", - body: new URLSearchParams({ - hash: current_hash, - urls: torrentTrackersTable.selectedRowsIds().map(encodeURIComponent).join("|") - }) - }) + method: "POST", + body: new URLSearchParams({ + hash: current_hash, + urls: torrentTrackersTable.selectedRowsIds().map(encodeURIComponent).join("|"), + }), + }) .then((response) => { if (!response.ok) return; @@ -312,15 +312,15 @@ window.qBittorrent.PropTrackers ??= (() => { return; const body = new URLSearchParams({ - hashes: current_hash + hashes: current_hash, }); if (trackers.length > 0) body.set("urls", trackers.map(encodeURIComponent).join("|")); fetch("api/v2/torrents/reannounce", { - method: "POST", - body: body - }) + method: "POST", + body: body, + }) .then((response) => { if (!response.ok) return; diff --git a/src/webui/www/private/scripts/prop-webseeds.js b/src/webui/www/private/scripts/prop-webseeds.ts similarity index 91% rename from src/webui/www/private/scripts/prop-webseeds.js rename to src/webui/www/private/scripts/prop-webseeds.ts index 495fed0f4b13..cc216cab02e8 100644 --- a/src/webui/www/private/scripts/prop-webseeds.js +++ b/src/webui/www/private/scripts/prop-webseeds.ts @@ -26,14 +26,12 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.PropWebseeds ??= (() => { const exports = () => { return { updateData: updateData, - clear: clear + clear: clear, }; }; @@ -45,8 +43,10 @@ window.qBittorrent.PropWebseeds ??= (() => { const loadWebSeedsData = () => { if (document.hidden) return; - if (document.getElementById("propWebSeeds").classList.contains("invisible") - || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { + if ( + document.getElementById("propWebSeeds").classList.contains("invisible") + || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand") + ) { // Tab changed, don't do anything return; } @@ -61,14 +61,14 @@ window.qBittorrent.PropWebseeds ??= (() => { current_hash = new_hash; } - const url = new URL("api/v2/torrents/webseeds", window.location); + const url = new URL("api/v2/torrents/webseeds", window.location.href); url.search = new URLSearchParams({ - hash: current_hash - }); + hash: current_hash, + }).toString(); fetch(url, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -94,7 +94,7 @@ window.qBittorrent.PropWebseeds ??= (() => { }) .finally(() => { clearTimeout(loadWebSeedsDataTimer); - loadWebSeedsDataTimer = loadWebSeedsData.delay(10000); + loadWebSeedsDataTimer = window.setTimeout(loadWebSeedsData, 10000); }); }; @@ -118,11 +118,11 @@ window.qBittorrent.PropWebseeds ??= (() => { }, RemoveWebSeed: (element, ref) => { removeWebSeedFN(element); - } + }, }, offsets: { x: 0, - y: 2 + y: 2, }, onShow: function() { const selectedWebseeds = torrentWebseedsTable.selectedRowsIds(); @@ -141,7 +141,7 @@ window.qBittorrent.PropWebseeds ??= (() => { this.showItem("RemoveWebSeed"); this.showItem("CopyWebseedUrl"); } - } + }, }); const addWebseedFN = () => { @@ -163,7 +163,7 @@ window.qBittorrent.PropWebseeds ??= (() => { height: 260, onCloseComplete: () => { updateData(); - } + }, }); }; @@ -192,7 +192,7 @@ window.qBittorrent.PropWebseeds ??= (() => { height: 150, onCloseComplete: () => { updateData(); - } + }, }); }; @@ -201,12 +201,12 @@ window.qBittorrent.PropWebseeds ??= (() => { return; fetch("api/v2/torrents/removeWebSeeds", { - method: "POST", - body: new URLSearchParams({ - hash: current_hash, - urls: torrentWebseedsTable.selectedRowsIds().map(webseed => encodeURIComponent(webseed)).join("|") - }) - }) + method: "POST", + body: new URLSearchParams({ + hash: current_hash, + urls: torrentWebseedsTable.selectedRowsIds().map(webseed => encodeURIComponent(webseed)).join("|"), + }), + }) .then((response) => { if (!response.ok) return; diff --git a/src/webui/www/private/scripts/rename-files.js b/src/webui/www/private/scripts/rename-files.ts similarity index 94% rename from src/webui/www/private/scripts/rename-files.js rename to src/webui/www/private/scripts/rename-files.ts index 3d29de70d6b6..3ad21f0ac7fb 100644 --- a/src/webui/www/private/scripts/rename-files.js +++ b/src/webui/www/private/scripts/rename-files.ts @@ -1,18 +1,16 @@ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.MultiRename ??= (() => { const exports = () => { return { AppliesTo: AppliesTo, - RenameFiles: RenameFiles + RenameFiles: RenameFiles, }; }; const AppliesTo = { FilenameExtension: "FilenameExtension", Filename: "Filename", - Extension: "Extension" + Extension: "Extension", }; class RenameFiles { @@ -151,7 +149,6 @@ window.qBittorrent.MultiRename ??= (() => { // Ignore files if (!row.isFolder && !this.includeFiles) continue; - // Ignore folders else if (row.isFolder && !this.includeFolders) continue; @@ -233,20 +230,16 @@ window.qBittorrent.MultiRename ??= (() => { const isFolder = match.isFolder; const parentPath = window.qBittorrent.Filesystem.folderName(match.path); - const oldPath = parentPath - ? parentPath + window.qBittorrent.Filesystem.PathSeparator + match.original - : match.original; - const newPath = parentPath - ? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName - : newName; + const oldPath = parentPath ? parentPath + window.qBittorrent.Filesystem.PathSeparator + match.original : match.original; + const newPath = parentPath ? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName : newName; try { - await fetch((isFolder ? "api/v2/torrents/renameFolder" : "api/v2/torrents/renameFile"), { + await fetch(isFolder ? "api/v2/torrents/renameFolder" : "api/v2/torrents/renameFile", { method: "POST", body: new URLSearchParams({ hash: this.hash, oldPath: oldPath, - newPath: newPath - }) + newPath: newPath, + }), }); replaced.push(match); } diff --git a/src/webui/www/private/scripts/search.js b/src/webui/www/private/scripts/search.ts similarity index 80% rename from src/webui/www/private/scripts/search.js rename to src/webui/www/private/scripts/search.ts index 90c099467149..15e7613164ad 100644 --- a/src/webui/www/private/scripts/search.js +++ b/src/webui/www/private/scripts/search.ts @@ -21,8 +21,6 @@ * THE SOFTWARE. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.Search ??= (() => { const exports = () => { @@ -75,68 +73,74 @@ window.qBittorrent.Search ??= (() => { const searchState = new Map(); const searchText = { pattern: "", - filterPattern: "" + filterPattern: "", }; const searchSeedsFilter = { min: 0, - max: 0 + max: 0, }; const searchSizeFilter = { min: 0, minUnit: 2, // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6 max: 0, - maxUnit: 3 + maxUnit: 3, }; const searchResultsTabsContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ targets: ".searchTab", menu: "searchResultsTabsMenu", actions: { - refreshTab: (tab) => { refreshSearch(tab); }, - closeTab: (tab) => { closeSearchTab(tab); }, + refreshTab: (tab) => { + refreshSearch(tab); + }, + closeTab: (tab) => { + closeSearchTab(tab); + }, closeAllTabs: () => { for (const tab of document.querySelectorAll("#searchTabs .searchTab")) closeSearchTab(tab); - } + }, }, offsets: { x: 2, - y: -60 + y: -60, }, onShow: function() { setActiveTab(this.options.element); - } + }, }); const init = () => { // load "Search in" preference from local storage - document.getElementById("searchInTorrentName").value = (localPreferences.get("search_in_filter") === "names") ? "names" : "everywhere"; + window.qBittorrent.Misc.getElementById("searchInTorrentName", "input").value = (localPreferences.get("search_in_filter") === "names") ? "names" : "everywhere"; const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ targets: "#searchResultsTableDiv tbody tr", menu: "searchResultsTableMenu", actions: { Download: downloadSearchTorrent, - OpenDescriptionUrl: openSearchTorrentDescriptionUrl + OpenDescriptionUrl: openSearchTorrentDescriptionUrl, }, offsets: { x: 0, - y: -60 - } + y: -60, + }, }); searchResultsTable = new window.qBittorrent.DynamicTable.SearchResultsTable(); searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu); getPlugins(); - searchResultsTable.dynamicTableDiv.addEventListener("dblclick", (e) => { downloadSearchTorrent(); }); + searchResultsTable.dynamicTableDiv.addEventListener("dblclick", (e) => { + downloadSearchTorrent(); + }); // listen for changes to searchInNameFilter let searchInNameFilterTimer = -1; document.getElementById("searchInNameFilter").addEventListener("input", (event) => { clearTimeout(searchInNameFilterTimer); - searchInNameFilterTimer = setTimeout(() => { + searchInNameFilterTimer = window.setTimeout(() => { searchInNameFilterTimer = -1; - const value = document.getElementById("searchInNameFilter").value; + const value = window.qBittorrent.Misc.getElementById("searchInNameFilter", "input").value; searchText.filterPattern = value; searchFilterChanged(); }, window.qBittorrent.Misc.FILTER_INPUT_DELAY); @@ -148,7 +152,7 @@ window.qBittorrent.Search ??= (() => { event.preventDefault(); event.stopPropagation(); - switch (event.target.id) { + switch ((event.target as HTMLElement).id) { case "manageSearchPlugins": manageSearchPlugins(); break; @@ -184,8 +188,8 @@ window.qBittorrent.Search ??= (() => { closeTabElem.alt = "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]"; closeTabElem.title = "QBT_TR(Close tab)QBT_TR[CONTEXT=SearchWidget]"; closeTabElem.src = "images/application-exit.svg"; - closeTabElem.width = "10"; - closeTabElem.height = "10"; + closeTabElem.width = 10; + closeTabElem.height = 10; closeTabElem.addEventListener("click", function(e) { e.stopPropagation(); closeSearchTab(this); @@ -239,8 +243,8 @@ window.qBittorrent.Search ??= (() => { fetch("api/v2/search/delete", { method: "POST", body: new URLSearchParams({ - id: oldSearchId - }) + id: oldSearchId, + }), }); const searchJobs = JSON.parse(localPreferences.get("search_jobs", "[]")); @@ -290,8 +294,8 @@ window.qBittorrent.Search ??= (() => { fetch("api/v2/search/delete", { method: "POST", body: new URLSearchParams({ - id: searchId - }) + id: String(searchId), + }), }); const searchJobs = JSON.parse(localPreferences.get("search_jobs", "[]")); @@ -305,8 +309,8 @@ window.qBittorrent.Search ??= (() => { resetSearchState(); resetFilters(); - document.getElementById("numSearchResultsVisible").textContent = 0; - document.getElementById("numSearchResultsTotal").textContent = 0; + document.getElementById("numSearchResultsVisible").textContent = "0"; + document.getElementById("numSearchResultsTotal").textContent = "0"; document.getElementById("searchResultsNoSearches").classList.remove("invisible"); document.getElementById("searchResultsFilters").classList.add("invisible"); document.getElementById("searchResultsTableContainer").classList.add("invisible"); @@ -372,23 +376,23 @@ window.qBittorrent.Search ??= (() => { // restore filters searchText.pattern = state.searchPattern; searchText.filterPattern = state.filterPattern; - document.getElementById("searchInNameFilter").value = state.filterPattern; + window.qBittorrent.Misc.getElementById("searchInNameFilter", "input").value = state.filterPattern; searchSeedsFilter.min = state.seedsFilter.min; searchSeedsFilter.max = state.seedsFilter.max; - document.getElementById("searchMinSeedsFilter").value = state.seedsFilter.min; - document.getElementById("searchMaxSeedsFilter").value = state.seedsFilter.max; + window.qBittorrent.Misc.getElementById("searchMinSeedsFilter", "input").value = state.seedsFilter.min; + window.qBittorrent.Misc.getElementById("searchMaxSeedsFilter", "input").value = state.seedsFilter.max; searchSizeFilter.min = state.sizeFilter.min; searchSizeFilter.minUnit = state.sizeFilter.minUnit; searchSizeFilter.max = state.sizeFilter.max; searchSizeFilter.maxUnit = state.sizeFilter.maxUnit; - document.getElementById("searchMinSizeFilter").value = state.sizeFilter.min; - document.getElementById("searchMinSizePrefix").value = state.sizeFilter.minUnit; - document.getElementById("searchMaxSizeFilter").value = state.sizeFilter.max; - document.getElementById("searchMaxSizePrefix").value = state.sizeFilter.maxUnit; + window.qBittorrent.Misc.getElementById("searchMinSizeFilter", "input").value = state.sizeFilter.min; + window.qBittorrent.Misc.getElementById("searchMinSizePrefix", "input").value = state.sizeFilter.minUnit; + window.qBittorrent.Misc.getElementById("searchMaxSizeFilter", "input").value = state.sizeFilter.max; + window.qBittorrent.Misc.getElementById("searchMaxSizePrefix", "input").value = state.sizeFilter.maxUnit; - const currentSearchPattern = document.getElementById("searchPattern").value.trim(); + const currentSearchPattern = window.qBittorrent.Misc.getElementById("searchPattern", "input").value.trim(); if (state.running && (state.searchPattern === currentSearchPattern)) { // allow search to be stopped document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]"; @@ -397,7 +401,7 @@ window.qBittorrent.Search ??= (() => { searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse); - document.getElementById("searchInTorrentName").value = state.searchIn; + window.qBittorrent.Misc.getElementById("searchInTorrentName", "input").value = state.searchIn; } // must restore all filters before calling updateTable @@ -417,15 +421,15 @@ window.qBittorrent.Search ??= (() => { statusIcon.title = text; statusIcon.src = image; statusIcon.className = "statusIcon"; - statusIcon.width = "12"; - statusIcon.height = "12"; + statusIcon.width = 12; + statusIcon.height = 12; return statusIcon; }; const updateStatusIconElement = (searchId, text, image) => { const searchTab = document.getElementById(`${searchTabIdPrefix}${searchId}`); if (searchTab) { - const statusIcon = searchTab.querySelector(".statusIcon"); + const statusIcon = searchTab.querySelector(".statusIcon") as HTMLImageElement; statusIcon.alt = text; statusIcon.title = text; statusIcon.src = image; @@ -435,13 +439,13 @@ window.qBittorrent.Search ??= (() => { const startSearch = (pattern, category, plugins) => { searchPatternChanged = false; fetch("api/v2/search/start", { - method: "POST", - body: new URLSearchParams({ - pattern: pattern, - category: category, - plugins: plugins - }) - }) + method: "POST", + body: new URLSearchParams({ + pattern: pattern, + category: category, + plugins: plugins, + }), + }) .then(async (response) => { if (!response.ok) return; @@ -469,13 +473,13 @@ window.qBittorrent.Search ??= (() => { searchPatternChanged = false; fetch("api/v2/search/start", { - method: "POST", - body: new URLSearchParams({ - pattern: state.searchPattern, - category: document.getElementById("categorySelect").value, - plugins: document.getElementById("pluginsSelect").value - }) - }) + method: "POST", + body: new URLSearchParams({ + pattern: state.searchPattern, + category: window.qBittorrent.Misc.getElementById("categorySelect", "select").value, + plugins: window.qBittorrent.Misc.getElementById("pluginsSelect", "select").value, + }), + }) .then(async (response) => { if (!response.ok) return; @@ -490,11 +494,11 @@ window.qBittorrent.Search ??= (() => { const stopSearch = (searchId) => { fetch("api/v2/search/stop", { - method: "POST", - body: new URLSearchParams({ - id: searchId - }) - }) + method: "POST", + body: new URLSearchParams({ + id: searchId, + }), + }) .then((response) => { if (!response.ok) return; @@ -515,9 +519,9 @@ window.qBittorrent.Search ??= (() => { const state = searchState.get(currentSearchId); const isSearchRunning = state && state.running; if (!isSearchRunning || searchPatternChanged) { - const pattern = document.getElementById("searchPattern").value.trim(); - const category = document.getElementById("categorySelect").value; - const plugins = document.getElementById("pluginsSelect").value; + const pattern = window.qBittorrent.Misc.getElementById("searchPattern", "input").value.trim(); + const category = window.qBittorrent.Misc.getElementById("categorySelect", "select").value; + const plugins = window.qBittorrent.Misc.getElementById("pluginsSelect", "select").value; if (!pattern || !category || !plugins) return; @@ -562,7 +566,7 @@ window.qBittorrent.Search ??= (() => { const downloadSearchTorrent = () => { for (const rowID of searchResultsTable.selectedRowsIds()) { const { engineName, fileName, fileUrl } = searchResultsTable.getRow(rowID).full_data; - qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl, undefined, engineName); + window.qBittorrent.Client.createAddTorrentWindow(fileName, fileUrl, undefined, engineName); } }; @@ -590,20 +594,20 @@ window.qBittorrent.Search ??= (() => { onClose: () => { clearTimeout(loadSearchPluginsTimer); loadSearchPluginsTimer = -1; - } + }, }); } }; const loadSearchPlugins = () => { getPlugins(); - loadSearchPluginsTimer = loadSearchPlugins.delay(2000); + loadSearchPluginsTimer = window.setTimeout(loadSearchPlugins, 2000); }; const onSearchPatternChanged = () => { const currentSearchId = getSelectedSearchId(); const state = searchState.get(currentSearchId); - const currentSearchPattern = document.getElementById("searchPattern").value.trim(); + const currentSearchPattern = window.qBittorrent.Misc.getElementById("searchPattern", "input").value.trim(); // start a new search if pattern has changed, otherwise allow the search to be stopped if (state && (state.searchPattern === currentSearchPattern)) { searchPatternChanged = false; @@ -616,11 +620,11 @@ window.qBittorrent.Search ??= (() => { }; const categorySelected = () => { - selectedCategory = document.getElementById("categorySelect").value; + selectedCategory = window.qBittorrent.Misc.getElementById("categorySelect", "select").value; }; const pluginSelected = () => { - selectedPlugin = document.getElementById("pluginsSelect").value; + selectedPlugin = window.qBittorrent.Misc.getElementById("pluginsSelect", "select").value; if (selectedPlugin !== prevSelectedPlugin) { prevSelectedPlugin = selectedPlugin; @@ -629,24 +633,24 @@ window.qBittorrent.Search ??= (() => { }; const reselectCategory = () => { - for (let i = 0; i < document.getElementById("categorySelect").options.length; ++i) { - if (document.getElementById("categorySelect").options[i].get("value") === selectedCategory) - document.getElementById("categorySelect").options[i].selected = true; + for (const option of window.qBittorrent.Misc.getElementById("categorySelect", "select").options) { + if (option.value === selectedCategory) + option.selected = true; } categorySelected(); }; const reselectPlugin = () => { - for (let i = 0; i < document.getElementById("pluginsSelect").options.length; ++i) { - if (document.getElementById("pluginsSelect").options[i].get("value") === selectedPlugin) - document.getElementById("pluginsSelect").options[i].selected = true; + for (const option of window.qBittorrent.Misc.getElementById("pluginsSelect", "select").options) { + if (option.value === selectedPlugin) + option.selected = true; } pluginSelected(); }; - const resetSearchState = (searchId) => { + const resetSearchState = (searchId = null) => { document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]"; const state = searchState.get(searchId); if (state) { @@ -679,7 +683,7 @@ window.qBittorrent.Search ??= (() => { document.getElementById("categorySelect").replaceChildren(...categoryOptions); }; - const selectedPlugin = document.getElementById("pluginsSelect").value; + const selectedPlugin = window.qBittorrent.Misc.getElementById("pluginsSelect", "select").value; if ((selectedPlugin === "all") || (selectedPlugin === "enabled")) { const uniqueCategories = {}; @@ -706,9 +710,9 @@ window.qBittorrent.Search ??= (() => { const getPlugins = () => { fetch("api/v2/search/plugins", { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) return; @@ -734,7 +738,7 @@ window.qBittorrent.Search ??= (() => { pluginOptions.push(createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled")); pluginOptions.push(createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all")); - const searchPluginsEmpty = (searchPlugins.length === 0); + const searchPluginsEmpty = searchPlugins.length === 0; if (!searchPluginsEmpty) { document.getElementById("searchResultsNoPlugins").classList.add("invisible"); if (numSearchTabs() === 0) @@ -747,10 +751,10 @@ window.qBittorrent.Search ??= (() => { return window.qBittorrent.Misc.naturalSortCollator.compare(leftName, rightName); }); - allPlugins.each((plugin) => { + for (const plugin of allPlugins) { if (plugin.enabled === true) pluginOptions.push(createOption(plugin.fullName, plugin.name)); - }); + } if (pluginOptions.length > 2) pluginOptions.splice(2, 0, createOption("──────────", undefined, true)); @@ -758,10 +762,10 @@ window.qBittorrent.Search ??= (() => { document.getElementById("pluginsSelect").replaceChildren(...pluginOptions); - document.getElementById("searchPattern").disabled = searchPluginsEmpty; - document.getElementById("categorySelect").disabled = searchPluginsEmpty; - document.getElementById("pluginsSelect").disabled = searchPluginsEmpty; - document.getElementById("startSearchButton").disabled = searchPluginsEmpty; + window.qBittorrent.Misc.getElementById("searchPattern", "input").disabled = searchPluginsEmpty; + window.qBittorrent.Misc.getElementById("categorySelect", "select").disabled = searchPluginsEmpty; + window.qBittorrent.Misc.getElementById("pluginsSelect", "select").disabled = searchPluginsEmpty; + window.qBittorrent.Misc.getElementById("startSearchButton", "button").disabled = searchPluginsEmpty; if (window.qBittorrent.SearchPlugins !== undefined) window.qBittorrent.SearchPlugins.updateTable(); @@ -782,25 +786,25 @@ window.qBittorrent.Search ??= (() => { const resetFilters = () => { searchText.filterPattern = ""; - document.getElementById("searchInNameFilter").value = ""; + window.qBittorrent.Misc.getElementById("searchInNameFilter", "input").value = ""; searchSeedsFilter.min = 0; searchSeedsFilter.max = 0; - document.getElementById("searchMinSeedsFilter").value = searchSeedsFilter.min; - document.getElementById("searchMaxSeedsFilter").value = searchSeedsFilter.max; + window.qBittorrent.Misc.getElementById("searchMinSeedsFilter", "input").value = String(searchSeedsFilter.min); + window.qBittorrent.Misc.getElementById("searchMaxSeedsFilter", "input").value = String(searchSeedsFilter.max); searchSizeFilter.min = 0; searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6 searchSizeFilter.max = 0; searchSizeFilter.maxUnit = 3; - document.getElementById("searchMinSizeFilter").value = searchSizeFilter.min; - document.getElementById("searchMinSizePrefix").value = searchSizeFilter.minUnit; - document.getElementById("searchMaxSizeFilter").value = searchSizeFilter.max; - document.getElementById("searchMaxSizePrefix").value = searchSizeFilter.maxUnit; + window.qBittorrent.Misc.getElementById("searchMinSizeFilter", "input").value = String(searchSizeFilter.min); + window.qBittorrent.Misc.getElementById("searchMinSizePrefix", "input").value = String(searchSizeFilter.minUnit); + window.qBittorrent.Misc.getElementById("searchMaxSizeFilter", "input").value = String(searchSizeFilter.max); + window.qBittorrent.Misc.getElementById("searchMaxSizePrefix", "input").value = String(searchSizeFilter.maxUnit); }; const getSearchInTorrentName = () => { - return (document.getElementById("searchInTorrentName").value === "names") ? "names" : "everywhere"; + return (window.qBittorrent.Misc.getElementById("searchInTorrentName", "input").value === "names") ? "names" : "everywhere"; }; const searchInTorrentName = () => { @@ -809,23 +813,23 @@ window.qBittorrent.Search ??= (() => { }; const searchSeedsFilterChanged = () => { - searchSeedsFilter.min = document.getElementById("searchMinSeedsFilter").value; - searchSeedsFilter.max = document.getElementById("searchMaxSeedsFilter").value; + searchSeedsFilter.min = Number(window.qBittorrent.Misc.getElementById("searchMinSeedsFilter", "input").value); + searchSeedsFilter.max = Number(window.qBittorrent.Misc.getElementById("searchMaxSeedsFilter", "input").value); searchFilterChanged(); }; const searchSizeFilterChanged = () => { - searchSizeFilter.min = document.getElementById("searchMinSizeFilter").value; - searchSizeFilter.minUnit = document.getElementById("searchMinSizePrefix").value; - searchSizeFilter.max = document.getElementById("searchMaxSizeFilter").value; - searchSizeFilter.maxUnit = document.getElementById("searchMaxSizePrefix").value; + searchSizeFilter.min = Number(window.qBittorrent.Misc.getElementById("searchMinSizeFilter", "input").value); + searchSizeFilter.minUnit = Number(window.qBittorrent.Misc.getElementById("searchMinSizePrefix", "input").value); + searchSizeFilter.max = Number(window.qBittorrent.Misc.getElementById("searchMaxSizeFilter", "input").value); + searchSizeFilter.maxUnit = Number(window.qBittorrent.Misc.getElementById("searchMaxSizePrefix", "input").value); searchFilterChanged(); }; const searchSizeFilterPrefixChanged = () => { - if ((Number(document.getElementById("searchMinSizeFilter").value) !== 0) || (Number(document.getElementById("searchMaxSizeFilter").value) !== 0)) + if ((Number(window.qBittorrent.Misc.getElementById("searchMinSizeFilter", "input").value) !== 0) || (Number(window.qBittorrent.Misc.getElementById("searchMaxSizeFilter", "input").value) !== 0)) searchSizeFilterChanged(); }; @@ -834,18 +838,18 @@ window.qBittorrent.Search ??= (() => { document.getElementById("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; }; - const loadSearchResultsData = function(searchId) { - const state = searchState.get(searchId); - const url = new URL("api/v2/search/results", window.location); + const loadSearchResultsData = (searchId) => { + let state = searchState.get(searchId); + const url = new URL("api/v2/search/results", window.location.href); url.search = new URLSearchParams({ id: searchId, - limit: 500, - offset: state.rowId - }); + limit: "500", + offset: state.rowId, + }).toString(); fetch(url, { - method: "GET", - cache: "no-store" - }) + method: "GET", + cache: "no-store", + }) .then(async (response) => { if (!response.ok) { if ((response.status === 400) || (response.status === 404)) { @@ -855,14 +859,16 @@ window.qBittorrent.Search ??= (() => { } else { clearTimeout(state.loadResultsTimer); - state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId); + state.loadResultsTimer = window.setTimeout(() => { + loadSearchResultsData(searchId); + }, 3000); } return; } document.getElementById("error_div").textContent = ""; - const state = searchState.get(searchId); + state = searchState.get(searchId); // check if user stopped the search prior to receiving the response if (!state.running) { clearTimeout(state.loadResultsTimer); @@ -916,19 +922,25 @@ window.qBittorrent.Search ??= (() => { } clearTimeout(state.loadResultsTimer); - state.loadResultsTimer = loadSearchResultsData.delay(2000, this, searchId); + state.loadResultsTimer = window.setTimeout(() => { + loadSearchResultsData(searchId); + }, 2000); }, (error) => { console.error(error); clearTimeout(state.loadResultsTimer); - state.loadResultsTimer = loadSearchResultsData.delay(3000, this, searchId); + state.loadResultsTimer = window.setTimeout(() => { + loadSearchResultsData(searchId); + }, 3000); }); }; - const updateSearchResultsData = function(searchId) { + const updateSearchResultsData = (searchId) => { const state = searchState.get(searchId); clearTimeout(state.loadResultsTimer); - state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId); + state.loadResultsTimer = window.setTimeout(() => { + loadSearchResultsData(searchId); + }, 500); }; for (const element of document.getElementsByClassName("copySearchDataToClipboard")) { diff --git a/src/webui/www/private/scripts/statistics.js b/src/webui/www/private/scripts/statistics.ts similarity index 94% rename from src/webui/www/private/scripts/statistics.js rename to src/webui/www/private/scripts/statistics.ts index a3b8448d9e75..f468f1a03e54 100644 --- a/src/webui/www/private/scripts/statistics.js +++ b/src/webui/www/private/scripts/statistics.ts @@ -21,8 +21,6 @@ * THE SOFTWARE. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.Statistics ??= (() => { const exports = () => { @@ -69,13 +67,13 @@ window.qBittorrent.Statistics ??= (() => { document.getElementById("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(statistics.alltimeDL, false); document.getElementById("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(statistics.alltimeUL, false); document.getElementById("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(statistics.totalWastedSession, false); - document.getElementById("GlobalRatio").textContent = statistics.globalRatio; - document.getElementById("TotalPeerConnections").textContent = statistics.totalPeerConnections; + document.getElementById("GlobalRatio").textContent = String(statistics.globalRatio); + document.getElementById("TotalPeerConnections").textContent = String(statistics.totalPeerConnections); document.getElementById("ReadCacheHits").textContent = `${statistics.readCacheHits}%`; document.getElementById("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(statistics.totalBuffersSize, false); document.getElementById("WriteCacheOverload").textContent = `${statistics.writeCacheOverload}%`; document.getElementById("ReadCacheOverload").textContent = `${statistics.readCacheOverload}%`; - document.getElementById("QueuedIOJobs").textContent = statistics.queuedIOJobs; + document.getElementById("QueuedIOJobs").textContent = String(statistics.queuedIOJobs); document.getElementById("AverageTimeInQueue").textContent = `${statistics.averageTimeInQueue} ms`; document.getElementById("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(statistics.totalQueuedSize, false); }; diff --git a/src/webui/www/private/scripts/torrent-content.js b/src/webui/www/private/scripts/torrent-content.ts similarity index 92% rename from src/webui/www/private/scripts/torrent-content.js rename to src/webui/www/private/scripts/torrent-content.ts index 7aa5ea577956..b68d73181cf7 100644 --- a/src/webui/www/private/scripts/torrent-content.js +++ b/src/webui/www/private/scripts/torrent-content.ts @@ -26,8 +26,6 @@ * exception statement from your version. */ -"use strict"; - window.qBittorrent ??= {}; window.qBittorrent.TorrentContent ??= (() => { const exports = () => { @@ -40,7 +38,7 @@ window.qBittorrent.TorrentContent ??= (() => { createPriorityCombo: createPriorityCombo, updatePriorityCombo: updatePriorityCombo, updateData: updateData, - clearFilterInputTimer: clearFilterInputTimer + clearFilterInputTimer: clearFilterInputTimer, }; }; @@ -102,7 +100,7 @@ window.qBittorrent.TorrentContent ??= (() => { return { rowIds: rowIds, - fileIds: fileIds + fileIds: fileIds, }; }; @@ -178,13 +176,13 @@ window.qBittorrent.TorrentContent ??= (() => { select.classList.add("combo_priority"); select.addEventListener("change", fileComboboxChanged); - select.appendChild(createOption(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]")); - select.appendChild(createOption(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]")); - select.appendChild(createOption(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]")); - select.appendChild(createOption(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]")); + select.appendChild(createOption(FilePriority.Ignored, FilePriority.Ignored === selectedPriority, "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]")); + select.appendChild(createOption(FilePriority.Normal, FilePriority.Normal === selectedPriority, "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]")); + select.appendChild(createOption(FilePriority.High, FilePriority.High === selectedPriority, "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]")); + select.appendChild(createOption(FilePriority.Maximum, FilePriority.Maximum === selectedPriority, "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]")); // "Mixed" priority is for display only; it shouldn't be selectable - const mixedPriorityOption = createOption(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]"); + const mixedPriorityOption = createOption(FilePriority.Mixed, FilePriority.Mixed === selectedPriority, "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]"); mixedPriorityOption.disabled = true; select.appendChild(mixedPriorityOption); @@ -212,7 +210,7 @@ window.qBittorrent.TorrentContent ??= (() => { const getComboboxPriority = (id) => { const node = torrentFilesTable.getNode(id.toString()); - return normalizePriority(node.priority, 10); + return normalizePriority(node.priority); }; const switchGlobalCheckboxState = (e) => { @@ -220,7 +218,7 @@ window.qBittorrent.TorrentContent ??= (() => { const rowIds = []; const fileIds = []; - const checkbox = document.getElementById("tristate_cb"); + const checkbox = document.getElementById("tristate_cb") as FileCheckbox; const priority = (checkbox.state === TriState.Checked) ? FilePriority.Ignored : FilePriority.Normal; if (checkbox.state === TriState.Checked) { @@ -229,7 +227,7 @@ window.qBittorrent.TorrentContent ??= (() => { const rowId = row.rowId; const node = torrentFilesTable.getNode(rowId); const fileId = node.fileId; - const isChecked = (node.checked === TriState.Checked); + const isChecked = node.checked === TriState.Checked; if (isChecked) { rowIds.push(rowId); fileIds.push(fileId); @@ -242,7 +240,7 @@ window.qBittorrent.TorrentContent ??= (() => { const rowId = row.rowId; const node = torrentFilesTable.getNode(rowId); const fileId = node.fileId; - const isUnchecked = (node.checked === TriState.Unchecked); + const isUnchecked = node.checked === TriState.Unchecked; if (isUnchecked) { rowIds.push(rowId); fileIds.push(fileId); @@ -304,16 +302,16 @@ window.qBittorrent.TorrentContent ??= (() => { const updateData = (files) => { const rows = files.map((file, index) => { - const ignore = (file.priority === FilePriority.Ignored); + const ignore = file.priority === FilePriority.Ignored; const row = { fileId: index, checked: (ignore ? TriState.Unchecked : TriState.Checked), fileName: file.name, name: window.qBittorrent.Filesystem.fileName(file.name), size: file.size, - progress: window.qBittorrent.Misc.toFixedPointString((file.progress * 100), 1), + progress: window.qBittorrent.Misc.toFixedPointString(file.progress * 100, 1), priority: normalizePriority(file.priority), - availability: file.availability + availability: file.availability, }; return row; @@ -351,9 +349,7 @@ window.qBittorrent.TorrentContent ??= (() => { if (folderNode === null) { folderNode = new window.qBittorrent.FileTree.FolderNode(); - folderNode.path = (parent.path === "") - ? folderName - : [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator); + folderNode.path = (parent.path === "") ? folderName : [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator); folderNode.name = folderName; folderNode.rowId = rowId; folderNode.root = parent; @@ -504,11 +500,11 @@ window.qBittorrent.TorrentContent ??= (() => { }, FilePrioMaximum: (element, ref) => { filesPriorityMenuClicked(FilePriority.Maximum); - } + }, }, offsets: { x: 0, - y: 2 + y: 2, }, }); @@ -534,10 +530,10 @@ window.qBittorrent.TorrentContent ??= (() => { clearTimeout(torrentFilesFilterInputTimer); torrentFilesFilterInputTimer = -1; - const value = document.getElementById("torrentFilesFilterInput").value; + const value = window.qBittorrent.Misc.getElementById("torrentFilesFilterInput", "input").value; torrentFilesTable.setFilter(value); - torrentFilesFilterInputTimer = setTimeout(() => { + torrentFilesFilterInputTimer = window.setTimeout(() => { torrentFilesFilterInputTimer = -1; torrentFilesTable.updateTable(); diff --git a/src/webui/www/private/scripts/types/qbittorrent.d.ts b/src/webui/www/private/scripts/types/qbittorrent.d.ts new file mode 100644 index 000000000000..179d907eae2f --- /dev/null +++ b/src/webui/www/private/scripts/types/qbittorrent.d.ts @@ -0,0 +1,43 @@ +declare global { + // Third-party libraries + const MochaUI: any; + const MUI: any; + const Asset: any; + const vanillaSelectBox: any; + const clipboardCopy: (text: string) => Promise; + + // Global variables used throughout the application + var torrentsTable: TorrentsTable; + var updateMainData: () => void; + + // Template string constant + const CACHEID: string; + const LANG: string; + + interface QBittorrent { + AddTorrent?: typeof addTorrentModule; + Cache?: typeof cacheModule; + ColorScheme?: typeof colorSchemeModule; + ContextMenu?: typeof contextMenuModule; + FileTree?: typeof fileTreeModule; + Misc?: typeof miscModule; + [key: string]: any; + } + + interface Window { + qBittorrent: QBittorrent; + } + + interface Element { + // MooTools + getSize(): { x: number; y: number }; + isVisible(): boolean; + } + + interface Document { + // MooTools + getSize(): { x: number; y: number }; + } +} + +export {}; diff --git a/src/webui/www/private/views/log.html b/src/webui/www/private/views/log.html index 2b51b5643f25..3a32ff7249d3 100644 --- a/src/webui/www/private/views/log.html +++ b/src/webui/www/private/views/log.html @@ -255,7 +255,7 @@ const syncLogWithInterval = (interval) => { if (!tableInfo[currentSelectedTab].progress) { clearTimeout(tableInfo[currentSelectedTab].timer); - tableInfo[currentSelectedTab].timer = syncLogData.delay(interval, null, currentSelectedTab); + tableInfo[currentSelectedTab].timer = window.setTimeout(() => { syncLogData(currentSelectedTab); }, interval); } }; @@ -292,7 +292,7 @@ const logFilterChanged = () => { clearTimeout(logFilterTimer); - logFilterTimer = setTimeout((curTab) => { + logFilterTimer = window.setTimeout((curTab) => { logFilterTimer = -1; tableInfo[curTab].instance.updateTable(false); diff --git a/src/webui/www/private/views/rss.html b/src/webui/www/private/views/rss.html index 26e27f77d213..dde740c90ac9 100644 --- a/src/webui/www/private/views/rss.html +++ b/src/webui/www/private/views/rss.html @@ -321,7 +321,7 @@ }; const load = () => { - feedRefreshTimer = setTimeout(() => { + feedRefreshTimer = window.setTimeout(() => { updateRssFeedList(); load(); }, serverSyncRssDataInterval); diff --git a/src/webui/www/private/views/torrentcreator.html b/src/webui/www/private/views/torrentcreator.html index c9346efab75c..16519cc761d3 100644 --- a/src/webui/www/private/views/torrentcreator.html +++ b/src/webui/www/private/views/torrentcreator.html @@ -218,7 +218,7 @@ const syncTaskWithInterval = (interval) => { clearTimeout(timer); - timer = setTimeout(syncTaskData, interval); + Timer = window.setTimeout(syncTaskData, interval); }; const syncTaskData = () => { diff --git a/src/webui/www/tsconfig.json b/src/webui/www/tsconfig.json new file mode 100644 index 000000000000..8ad214da7675 --- /dev/null +++ b/src/webui/www/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "outDir": "./private/scripts", + "rootDir": "./private/scripts", + + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + + "strict": true, + "noImplicitAny": false, + "strictNullChecks": false, + "strictFunctionTypes": true, + "strictBindCallApply": false, + "strictPropertyInitialization": false, + "noImplicitThis": false, + "alwaysStrict": true, + + "allowJs": false, + "checkJs": false, + + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "removeComments": false, + "preserveConstEnums": true, + + "forceConsistentCasingInFileNames": false, + "skipLibCheck": true, + "useDefineForClassFields": true + }, + "include": [ + "private/scripts/**/*.ts", + "private/scripts/types/*.d.ts" + ], + "exclude": [ + "node_modules", + "private/scripts/lib/**/*", + "private/scripts/**/*.js" + ] +}