This commit is contained in:
Thomas Piccirello 2025-08-17 21:15:19 +02:00 committed by GitHub
commit 46a13811e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 660 additions and 161 deletions

View file

@ -51,17 +51,20 @@
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/global.h" #include "base/global.h"
#include "base/interfaces/iapplication.h" #include "base/interfaces/iapplication.h"
#include "base/logger.h"
#include "base/net/downloadmanager.h" #include "base/net/downloadmanager.h"
#include "base/net/portforwarder.h" #include "base/net/portforwarder.h"
#include "base/net/proxyconfigurationmanager.h" #include "base/net/proxyconfigurationmanager.h"
#include "base/path.h" #include "base/path.h"
#include "base/preferences.h" #include "base/preferences.h"
#include "base/profile.h"
#include "base/rss/rss_autodownloader.h" #include "base/rss/rss_autodownloader.h"
#include "base/rss/rss_session.h" #include "base/rss/rss_session.h"
#include "base/torrentfileguard.h" #include "base/torrentfileguard.h"
#include "base/torrentfileswatcher.h" #include "base/torrentfileswatcher.h"
#include "base/utils/datetime.h" #include "base/utils/datetime.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
#include "base/utils/io.h"
#include "base/utils/misc.h" #include "base/utils/misc.h"
#include "base/utils/net.h" #include "base/utils/net.h"
#include "base/utils/password.h" #include "base/utils/password.h"
@ -84,6 +87,48 @@ const QString KEY_FILE_METADATA_CREATION_DATE = u"creation_date"_s;
const QString KEY_FILE_METADATA_LAST_ACCESS_DATE = u"last_access_date"_s; const QString KEY_FILE_METADATA_LAST_ACCESS_DATE = u"last_access_date"_s;
const QString KEY_FILE_METADATA_LAST_MODIFICATION_DATE = u"last_modification_date"_s; const QString KEY_FILE_METADATA_LAST_MODIFICATION_DATE = u"last_modification_date"_s;
const int CLIENT_DATA_FILE_MAX_SIZE = 1024 * 1024;
const QString CLIENT_DATA_FILE_NAME = u"web_data.json"_s;
Path AppController::m_clientDataFilePath;
QJsonObject AppController::m_clientData;
AppController::AppController(IApplication *app, QObject *parent)
: APIController(app, parent)
{
if (m_clientDataFilePath.isEmpty())
{
m_clientDataFilePath = specialFolderLocation(SpecialFolder::Data) / Path(CLIENT_DATA_FILE_NAME);
if (m_clientDataFilePath.exists())
{
const auto readResult = Utils::IO::readFile(m_clientDataFilePath, CLIENT_DATA_FILE_MAX_SIZE);
if (!readResult)
{
LogMsg(tr("Failed to load web client data. %1").arg(readResult.error().message), Log::WARNING);
return;
}
QJsonParseError jsonError;
const QJsonDocument jsonDoc = QJsonDocument::fromJson(readResult.value(), &jsonError);
if (jsonError.error != QJsonParseError::NoError)
{
LogMsg(tr("Failed to parse web client data. File: \"%1\". Error: \"%2\"")
.arg(m_clientDataFilePath.toString(), jsonError.errorString()), Log::WARNING);
return;
}
if (!jsonDoc.isObject())
{
LogMsg(tr("Failed to load web client data. File: \"%1\". Error: \"Invalid data format\"")
.arg(m_clientDataFilePath.toString()), Log::WARNING);
return;
}
m_clientData = jsonDoc.object();
}
}
}
void AppController::webapiVersionAction() void AppController::webapiVersionAction()
{ {
setResult(API_VERSION.toString()); setResult(API_VERSION.toString());
@ -1312,6 +1357,89 @@ void AppController::setCookiesAction()
setResult(QString()); setResult(QString());
} }
void AppController::clientDataAction()
{
const QString keysParam {params()[u"keys"_s]};
if (keysParam.isEmpty())
{
setResult(m_clientData);
return;
}
QJsonParseError jsonError;
const auto keysJsonDocument = QJsonDocument::fromJson(keysParam.toUtf8(), &jsonError);
if (jsonError.error != QJsonParseError::NoError)
throw APIError(APIErrorType::BadParams, jsonError.errorString());
if (!keysJsonDocument.isArray())
throw APIError(APIErrorType::BadParams, tr("keys must be an array"));
QJsonObject clientData;
for (const QJsonValue &keysJsonVal : asConst(keysJsonDocument.array()))
{
if (!keysJsonVal.isString())
throw APIError(APIErrorType::BadParams, tr("key must be a string"));
const QString &key = keysJsonVal.toString();
if (const auto iter = m_clientData.constFind(key); iter != m_clientData.constEnd())
clientData.insert(key, iter.value());
}
setResult(clientData);
}
void AppController::setClientDataAction()
{
requireParams({u"data"_s});
QJsonParseError jsonError;
const auto dataJsonDocument = QJsonDocument::fromJson(params()[u"data"_s].toUtf8(), &jsonError);
if (jsonError.error != QJsonParseError::NoError)
throw APIError(APIErrorType::BadParams, jsonError.errorString());
if (!dataJsonDocument.isObject())
throw APIError(APIErrorType::BadParams, tr("data must be an object"));
QJsonObject clientData = m_clientData;
bool dataModified = false;
const QJsonObject dataJsonObject = dataJsonDocument.object();
for (auto it = dataJsonObject.constBegin(), end = dataJsonObject.constEnd(); it != end; ++it)
{
const QString &key = it.key();
const QJsonValue &value = it.value();
if (value.isNull())
{
if (auto it = clientData.find(key); it != clientData.end())
{
clientData.erase(it);
dataModified = true;
}
}
else
{
const auto &existingValue = clientData.constFind(key);
if ((existingValue == clientData.constEnd()) || (existingValue.value() != value))
{
clientData.insert(key, value);
dataModified = true;
}
}
}
if (!dataModified)
return;
const QByteArray json = QJsonDocument(clientData).toJson(QJsonDocument::Compact);
if (json.size() > CLIENT_DATA_FILE_MAX_SIZE)
throw APIError(APIErrorType::BadParams, tr("data must not be larger than %1 bytes").arg(CLIENT_DATA_FILE_MAX_SIZE));
const nonstd::expected<void, QString> result = Utils::IO::saveToFile(m_clientDataFilePath, json);
if (!result)
{
throw APIError(APIErrorType::Conflict, tr("Failed to save web client data. Error: \"%1\"")
.arg(result.error()));
}
m_clientData = clientData;
}
void AppController::networkInterfaceListAction() void AppController::networkInterfaceListAction()
{ {
QJsonArray ifaceList; QJsonArray ifaceList;

View file

@ -30,7 +30,10 @@
#pragma once #pragma once
#include <QJsonObject>
#include "apicontroller.h" #include "apicontroller.h"
#include "base/path.h"
class AppController : public APIController class AppController : public APIController
{ {
@ -38,7 +41,7 @@ class AppController : public APIController
Q_DISABLE_COPY_MOVE(AppController) Q_DISABLE_COPY_MOVE(AppController)
public: public:
using APIController::APIController; explicit AppController(IApplication *app, QObject *parent = nullptr);
private slots: private slots:
void webapiVersionAction(); void webapiVersionAction();
@ -52,7 +55,13 @@ private slots:
void getDirectoryContentAction(); void getDirectoryContentAction();
void cookiesAction(); void cookiesAction();
void setCookiesAction(); void setCookiesAction();
void clientDataAction();
void setClientDataAction();
void networkInterfaceListAction(); void networkInterfaceListAction();
void networkInterfaceAddressListAction(); void networkInterfaceAddressListAction();
private:
static Path m_clientDataFilePath;
static QJsonObject m_clientData;
}; };

View file

@ -148,6 +148,7 @@ private:
{ {
// <<controller name, action name>, HTTP method> // <<controller name, action name>, HTTP method>
{{u"app"_s, u"sendTestEmail"_s}, Http::METHOD_POST}, {{u"app"_s, u"sendTestEmail"_s}, Http::METHOD_POST},
{{u"app"_s, u"setClientData"_s}, Http::METHOD_POST},
{{u"app"_s, u"setCookies"_s}, Http::METHOD_POST}, {{u"app"_s, u"setCookies"_s}, Http::METHOD_POST},
{{u"app"_s, u"setPreferences"_s}, Http::METHOD_POST}, {{u"app"_s, u"setPreferences"_s}, Http::METHOD_POST},
{{u"app"_s, u"shutdown"_s}, Http::METHOD_POST}, {{u"app"_s, u"shutdown"_s}, Http::METHOD_POST},

View file

@ -29,6 +29,7 @@
<script defer src="scripts/monkeypatch.js?v=${CACHEID}"></script> <script defer src="scripts/monkeypatch.js?v=${CACHEID}"></script>
<script defer src="scripts/cache.js?v=${CACHEID}"></script> <script defer src="scripts/cache.js?v=${CACHEID}"></script>
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script> <script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
<script defer src="scripts/client-data.js?v=${CACHEID}"></script>
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script> <script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
<script defer src="scripts/mocha-init.js?locale=${LANG}&v=${CACHEID}"></script> <script defer src="scripts/mocha-init.js?locale=${LANG}&v=${CACHEID}"></script>
<script defer src="scripts/lib/clipboard-copy.js"></script> <script defer src="scripts/lib/clipboard-copy.js"></script>

View file

@ -44,9 +44,10 @@ window.qBittorrent.AddTorrent ??= (() => {
let source = ""; let source = "";
const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences(); const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
const clientData = window.parent.qBittorrent.ClientData;
const getCategories = () => { const getCategories = () => {
const defaultCategory = LocalPreferences.get("add_torrent_default_category", ""); const defaultCategory = clientData.getCached("add_torrent_default_category") ?? "";
const categorySelect = document.getElementById("categorySelect"); const categorySelect = document.getElementById("categorySelect");
for (const name of window.parent.qBittorrent.Client.categoryMap.keys()) { for (const name of window.parent.qBittorrent.Client.categoryMap.keys()) {
const option = document.createElement("option"); const option = document.createElement("option");
@ -274,7 +275,7 @@ window.qBittorrent.AddTorrent ??= (() => {
if (metadata.info?.length !== undefined) if (metadata.info?.length !== undefined)
document.getElementById("size").textContent = window.qBittorrent.Misc.friendlyUnit(metadata.info.length, false); document.getElementById("size").textContent = window.qBittorrent.Misc.friendlyUnit(metadata.info.length, false);
if ((metadata.creation_date !== undefined) && (metadata.creation_date > 1)) if ((metadata.creation_date !== undefined) && (metadata.creation_date > 1))
document.getElementById("createdDate").textContent = new Date(metadata.creation_date * 1000).toLocaleString(); document.getElementById("createdDate").textContent = window.qBittorrent.Misc.formatDate(new Date(metadata.creation_date * 1000));
if (metadata.comment !== undefined) if (metadata.comment !== undefined)
document.getElementById("comment").textContent = metadata.comment; document.getElementById("comment").textContent = metadata.comment;
@ -311,10 +312,7 @@ window.qBittorrent.AddTorrent ??= (() => {
if (document.getElementById("setDefaultCategory").checked) { if (document.getElementById("setDefaultCategory").checked) {
const category = document.getElementById("category").value.trim(); const category = document.getElementById("category").value.trim();
if (category.length === 0) clientData.set({ add_torrent_default_category: category.length > 0 ? category : null });
LocalPreferences.remove("add_torrent_default_category");
else
LocalPreferences.set("add_torrent_default_category", category);
} }
}; };

View file

@ -76,12 +76,8 @@ window.qBittorrent.Cache ??= (() => {
class PreferencesCache { class PreferencesCache {
#m_store = {}; #m_store = {};
// obj: { async init() {
// onFailure: () => {}, return await fetch("api/v2/app/preferences", {
// onSuccess: () => {}
// }
init(obj = {}) {
return fetch("api/v2/app/preferences", {
method: "GET", method: "GET",
cache: "no-store" cache: "no-store"
}) })
@ -94,12 +90,7 @@ window.qBittorrent.Cache ??= (() => {
deepFreeze(responseJSON); deepFreeze(responseJSON);
this.#m_store = responseJSON; this.#m_store = responseJSON;
if (typeof obj.onSuccess === "function") return responseJSON;
obj.onSuccess(responseJSON, responseText);
},
(error) => {
if (typeof obj.onFailure === "function")
obj.onFailure(error);
}); });
} }
@ -107,45 +98,29 @@ window.qBittorrent.Cache ??= (() => {
return this.#m_store; return this.#m_store;
} }
// obj: { async set(data) {
// data: {}, if (typeof data !== "object")
// onFailure: () => {},
// onSuccess: () => {}
// }
set(obj) {
if (typeof obj !== "object")
throw new Error("`obj` is not an object.");
if (typeof obj.data !== "object")
throw new Error("`data` is not an object."); throw new Error("`data` is not an object.");
fetch("api/v2/app/setPreferences", { return await fetch("api/v2/app/setPreferences", {
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
json: JSON.stringify(obj.data) json: JSON.stringify(data)
}) })
}) })
.then(async (response) => { .then((response) => {
if (!response.ok) if (!response.ok)
return; return;
this.#m_store = structuredClone(this.#m_store); this.#m_store = structuredClone(this.#m_store);
for (const key in obj.data) { for (const key in data) {
if (!Object.hasOwn(obj.data, key)) if (!Object.hasOwn(data, key))
continue; continue;
const value = obj.data[key]; const value = data[key];
this.#m_store[key] = value; this.#m_store[key] = value;
} }
deepFreeze(this.#m_store); deepFreeze(this.#m_store);
if (typeof obj.onSuccess === "function") {
const responseText = await response.text();
obj.onSuccess(responseText);
}
},
(error) => {
if (typeof obj.onFailure === "function")
obj.onFailure(error);
}); });
} }
} }

View file

@ -0,0 +1,128 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Thomas Piccirello <thomas@piccirello.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
"use strict";
window.qBittorrent ??= {};
window.qBittorrent.ClientData ??= (() => {
const exports = () => {
return {
getCached: (...args) => instance.getCached(...args),
get: (...args) => instance.get(...args),
set: (...args) => instance.set(...args),
};
};
// this is exposed as a singleton
class ClientData {
/**
* @type Map<string, any>
*/
#cache = new Map();
/**
* @param {string[]} keys
* @returns {Record<string, any>}
*/
async #fetch(keys) {
return await fetch("api/v2/app/clientData", {
method: "POST",
body: new URLSearchParams({
keys: JSON.stringify(keys)
})
})
.then(async (response) => {
if (!response.ok)
return;
return await response.json();
});
}
/**
* @param {Record<string, any>} data
*/
async #set(data) {
await fetch("api/v2/app/setClientData", {
method: "POST",
body: new URLSearchParams({
data: JSON.stringify(data)
})
})
.then((response) => {
if (!response.ok)
throw new Error("Failed to store client data");
});
}
/**
* @param {string} key
* @returns {any}
*/
getCached(key) {
return this.#cache.get(key);
}
/**
* @param {string[]} keys
* @returns {Record<string, any>}
*/
async get(keys = []) {
const keysToFetch = keys.filter((key) => !this.#cache.has(key));
if (keysToFetch.length > 0) {
const fetchedData = await this.#fetch(keysToFetch);
for (const [key, value] of Object.entries(fetchedData))
this.#cache.set(key, value);
}
return Object.fromEntries(keys.map((key) => ([key, this.#cache.get(key)])));
}
/**
* @param {Record<string, any>} data
*/
async set(data) {
try {
await this.#set(data);
}
catch (err) {
console.error(err);
return;
}
// update cache
for (const [key, value] of Object.entries(data))
this.#cache.set(key, value);
}
}
const instance = new ClientData();
return exports();
})();
Object.freeze(window.qBittorrent.ClientData);

View file

@ -31,6 +31,7 @@ window.qBittorrent.Client ??= (() => {
return { return {
setup: setup, setup: setup,
initializeCaches: initializeCaches, initializeCaches: initializeCaches,
initializeClientData: initializeClientData,
closeWindow: closeWindow, closeWindow: closeWindow,
closeFrameWindow: closeFrameWindow, closeFrameWindow: closeFrameWindow,
getSyncMainDataInterval: getSyncMainDataInterval, getSyncMainDataInterval: getSyncMainDataInterval,
@ -56,15 +57,48 @@ window.qBittorrent.Client ??= (() => {
const tagMap = new Map(); const tagMap = new Map();
let cacheAllSettled; let cacheAllSettled;
let clientDataPromise;
const setup = () => { const setup = () => {
// fetch various data and store it in memory // fetch various data and store it in memory
clientDataPromise = window.qBittorrent.ClientData.get([
"show_search_engine",
"show_rss_reader",
"show_log_viewer",
"speed_in_browser_title_bar",
"show_top_toolbar",
"show_status_bar",
"show_filters_sidebar",
"hide_zero_status_filters",
"date_format",
"color_scheme",
"full_url_tracker_column",
"use_alt_row_colors",
"use_virtual_list",
"dblclick_complete",
"dblclick_download",
"dblclick_filter",
"search_in_filter",
"qbt_selected_log_levels",
"add_torrent_default_category",
]);
cacheAllSettled = Promise.allSettled([ cacheAllSettled = Promise.allSettled([
window.qBittorrent.Cache.buildInfo.init(), window.qBittorrent.Cache.buildInfo.init(),
window.qBittorrent.Cache.preferences.init(), window.qBittorrent.Cache.preferences.init(),
window.qBittorrent.Cache.qbtVersion.init() window.qBittorrent.Cache.qbtVersion.init(),
clientDataPromise,
]); ]);
}; };
const initializeClientData = async () => {
try {
await clientDataPromise;
}
catch (error) {
console.error(`Failed to initialize client data. Reason: "${error}".`);
}
};
const initializeCaches = async () => { const initializeCaches = async () => {
const results = await cacheAllSettled; const results = await cacheAllSettled;
for (const [idx, result] of results.entries()) { for (const [idx, result] of results.entries()) {
@ -224,8 +258,8 @@ let queueing_enabled = true;
let serverSyncMainDataInterval = 1500; let serverSyncMainDataInterval = 1500;
let customSyncMainDataInterval = null; let customSyncMainDataInterval = null;
let useSubcategories = true; let useSubcategories = true;
const useAutoHideZeroStatusFilters = LocalPreferences.get("hide_zero_status_filters", "false") === "true"; let useAutoHideZeroStatusFilters = false;
const displayFullURLTrackerColumn = LocalPreferences.get("full_url_tracker_column", "false") === "true"; let displayFullURLTrackerColumn = false;
/* Categories filter */ /* Categories filter */
const CATEGORIES_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655"; const CATEGORIES_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655";
@ -251,6 +285,8 @@ const TRACKERS_WARNING = "82a702c5-210c-412b-829f-97632d7557e9";
// Map<trackerHost: String, Map<trackerURL: String, torrents: Set>> // Map<trackerHost: String, Map<trackerURL: String, torrents: Set>>
const trackerMap = new Map(); const trackerMap = new Map();
const clientData = window.qBittorrent.ClientData;
let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL); let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
let setTrackerFilter = () => {}; let setTrackerFilter = () => {};
@ -259,7 +295,13 @@ let selectedStatus = LocalPreferences.get("selected_filter", "all");
let setStatusFilter = () => {}; let setStatusFilter = () => {};
let toggleFilterDisplay = () => {}; let toggleFilterDisplay = () => {};
window.addEventListener("DOMContentLoaded", (event) => { window.addEventListener("DOMContentLoaded", async (event) => {
await window.qBittorrent.Client.initializeClientData();
window.qBittorrent.ColorScheme.update();
useAutoHideZeroStatusFilters = clientData.getCached("hide_zero_status_filters") === true;
displayFullURLTrackerColumn = clientData.getCached("full_url_tracker_column") === true;
window.qBittorrent.LocalPreferences.upgrade(); window.qBittorrent.LocalPreferences.upgrade();
let isSearchPanelLoaded = false; let isSearchPanelLoaded = false;
@ -406,6 +448,13 @@ window.addEventListener("DOMContentLoaded", (event) => {
LocalPreferences.set(`filter_${filterListID.replace("FilterList", "")}_collapsed`, filterList.classList.toggle("invisible").toString()); LocalPreferences.set(`filter_${filterListID.replace("FilterList", "")}_collapsed`, filterList.classList.toggle("invisible").toString());
}; };
const highlightSelectedStatus = () => {
const statusFilter = document.getElementById("statusFilterList");
const filterID = `${selectedStatus}_filter`;
for (const status of statusFilter.children)
status.classList.toggle("selectedFilter", (status.id === filterID));
};
new MochaUI.Panel({ new MochaUI.Panel({
id: "Filters", id: "Filters",
title: "Panel", title: "Panel",
@ -427,35 +476,35 @@ window.addEventListener("DOMContentLoaded", (event) => {
initializeWindows(); initializeWindows();
// Show Top Toolbar is enabled by default // Show Top Toolbar is enabled by default
let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true"; let showTopToolbar = clientData.getCached("show_top_toolbar") !== false;
if (!showTopToolbar) { if (!showTopToolbar) {
document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0"; document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0";
document.getElementById("mochaToolbar").classList.add("invisible"); document.getElementById("mochaToolbar").classList.add("invisible");
} }
// Show Status Bar is enabled by default // Show Status Bar is enabled by default
let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true"; let showStatusBar = clientData.getCached("show_status_bar") !== false;
if (!showStatusBar) { if (!showStatusBar) {
document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0"; document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0";
document.getElementById("desktopFooterWrapper").classList.add("invisible"); document.getElementById("desktopFooterWrapper").classList.add("invisible");
} }
// Show Filters Sidebar is enabled by default // Show Filters Sidebar is enabled by default
let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true"; let showFiltersSidebar = clientData.getCached("show_filters_sidebar") !== false;
if (!showFiltersSidebar) { if (!showFiltersSidebar) {
document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0"; document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0";
document.getElementById("filtersColumn").classList.add("invisible"); document.getElementById("filtersColumn").classList.add("invisible");
document.getElementById("filtersColumn_handle").classList.add("invisible"); document.getElementById("filtersColumn_handle").classList.add("invisible");
} }
let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true"; let speedInTitle = clientData.getCached("speed_in_browser_title_bar") === true;
if (!speedInTitle) if (!speedInTitle)
document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0"; document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0";
// After showing/hiding the toolbar + status bar // After showing/hiding the toolbar + status bar
window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false"); window.qBittorrent.Client.showSearchEngine(clientData.getCached("show_search_engine") !== false);
window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false"); window.qBittorrent.Client.showRssReader(clientData.getCached("show_rss_reader") !== false);
window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true"); window.qBittorrent.Client.showLogViewer(clientData.getCached("show_log_viewer") === true);
// After Show Top Toolbar // After Show Top Toolbar
MochaUI.Desktop.setDesktopSize(); MochaUI.Desktop.setDesktopSize();
@ -572,13 +621,6 @@ window.addEventListener("DOMContentLoaded", (event) => {
window.qBittorrent.Filters.clearStatusFilter(); window.qBittorrent.Filters.clearStatusFilter();
}; };
const highlightSelectedStatus = () => {
const statusFilter = document.getElementById("statusFilterList");
const filterID = `${selectedStatus}_filter`;
for (const status of statusFilter.children)
status.classList.toggle("selectedFilter", (status.id === filterID));
};
const updateCategoryList = () => { const updateCategoryList = () => {
const categoryList = document.getElementById("categoryFilterList"); const categoryList = document.getElementById("categoryFilterList");
if (!categoryList) if (!categoryList)
@ -1221,7 +1263,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
document.getElementById("showTopToolbarLink").addEventListener("click", (e) => { document.getElementById("showTopToolbarLink").addEventListener("click", (e) => {
showTopToolbar = !showTopToolbar; showTopToolbar = !showTopToolbar;
LocalPreferences.set("show_top_toolbar", showTopToolbar.toString()); clientData.set({ show_top_toolbar: showTopToolbar });
if (showTopToolbar) { if (showTopToolbar) {
document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "1"; document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "1";
document.getElementById("mochaToolbar").classList.remove("invisible"); document.getElementById("mochaToolbar").classList.remove("invisible");
@ -1235,7 +1277,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
document.getElementById("showStatusBarLink").addEventListener("click", (e) => { document.getElementById("showStatusBarLink").addEventListener("click", (e) => {
showStatusBar = !showStatusBar; showStatusBar = !showStatusBar;
LocalPreferences.set("show_status_bar", showStatusBar.toString()); clientData.set({ show_status_bar: showStatusBar });
if (showStatusBar) { if (showStatusBar) {
document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "1"; document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "1";
document.getElementById("desktopFooterWrapper").classList.remove("invisible"); document.getElementById("desktopFooterWrapper").classList.remove("invisible");
@ -1272,7 +1314,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
document.getElementById("showFiltersSidebarLink").addEventListener("click", (e) => { document.getElementById("showFiltersSidebarLink").addEventListener("click", (e) => {
showFiltersSidebar = !showFiltersSidebar; showFiltersSidebar = !showFiltersSidebar;
LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString()); clientData.set({ show_filters_sidebar: showFiltersSidebar });
if (showFiltersSidebar) { if (showFiltersSidebar) {
document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "1"; document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "1";
document.getElementById("filtersColumn").classList.remove("invisible"); document.getElementById("filtersColumn").classList.remove("invisible");
@ -1288,7 +1330,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
document.getElementById("speedInBrowserTitleBarLink").addEventListener("click", (e) => { document.getElementById("speedInBrowserTitleBarLink").addEventListener("click", (e) => {
speedInTitle = !speedInTitle; speedInTitle = !speedInTitle;
LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString()); LocalPreferences.set("speed_in_browser_title_bar", speedInTitle);
if (speedInTitle) if (speedInTitle)
document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "1"; document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "1";
else else
@ -1298,19 +1340,19 @@ window.addEventListener("DOMContentLoaded", (event) => {
document.getElementById("showSearchEngineLink").addEventListener("click", (e) => { document.getElementById("showSearchEngineLink").addEventListener("click", (e) => {
window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine()); window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString()); clientData.set({ show_search_engine: window.qBittorrent.Client.isShowSearchEngine() });
updateTabDisplay(); updateTabDisplay();
}); });
document.getElementById("showRssReaderLink").addEventListener("click", (e) => { document.getElementById("showRssReaderLink").addEventListener("click", (e) => {
window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader()); window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString()); clientData.set({ show_rss_reader: window.qBittorrent.Client.isShowRssReader() });
updateTabDisplay(); updateTabDisplay();
}); });
document.getElementById("showLogViewerLink").addEventListener("click", (e) => { document.getElementById("showLogViewerLink").addEventListener("click", (e) => {
window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer()); window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString()); clientData.set({ show_log_viewer: window.qBittorrent.Client.isShowLogViewer() });
updateTabDisplay(); updateTabDisplay();
}); });
@ -1367,7 +1409,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
// main window tabs // main window tabs
const showTransfersTab = () => { const showTransfersTab = () => {
const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true"; const showFiltersSidebar = clientData.getCached("show_filters_sidebar") !== false;
if (showFiltersSidebar) { if (showFiltersSidebar) {
document.getElementById("filtersColumn").classList.remove("invisible"); document.getElementById("filtersColumn").classList.remove("invisible");
document.getElementById("filtersColumn_handle").classList.remove("invisible"); document.getElementById("filtersColumn_handle").classList.remove("invisible");

View file

@ -36,12 +36,12 @@ window.qBittorrent.ColorScheme ??= (() => {
}; };
}; };
const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)"); const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)");
const clientData = window.parent.qBittorrent.ClientData;
const update = () => { const update = () => {
const root = document.documentElement; const root = document.documentElement;
const colorScheme = LocalPreferences.get("color_scheme"); const colorScheme = clientData.getCached("color_scheme");
const validScheme = (colorScheme === "light") || (colorScheme === "dark"); const validScheme = (colorScheme === "light") || (colorScheme === "dark");
const isDark = colorSchemeQuery.matches; const isDark = colorSchemeQuery.matches;
root.classList.toggle("dark", ((!validScheme && isDark) || (colorScheme === "dark"))); root.classList.toggle("dark", ((!validScheme && isDark) || (colorScheme === "dark")));
@ -52,5 +52,3 @@ window.qBittorrent.ColorScheme ??= (() => {
return exports(); return exports();
})(); })();
Object.freeze(window.qBittorrent.ColorScheme); Object.freeze(window.qBittorrent.ColorScheme);
window.qBittorrent.ColorScheme.update();

View file

@ -65,6 +65,7 @@ window.qBittorrent.DynamicTable ??= (() => {
return 0; return 0;
}; };
const clientData = window.qBittorrent.ClientData ?? window.parent.qBittorrent.ClientData;
let DynamicTableHeaderContextMenuClass = null; let DynamicTableHeaderContextMenuClass = null;
if (typeof LocalPreferences === "undefined") if (typeof LocalPreferences === "undefined")
@ -75,7 +76,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.dynamicTableDivId = dynamicTableDivId; this.dynamicTableDivId = dynamicTableDivId;
this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId; this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
this.dynamicTableDiv = document.getElementById(dynamicTableDivId); this.dynamicTableDiv = document.getElementById(dynamicTableDivId);
this.useVirtualList = useVirtualList && (LocalPreferences.get("use_virtual_list", "false") === "true"); this.useVirtualList = useVirtualList && (clientData.getCached("use_virtual_list") === true);
this.fixedTableHeader = document.querySelector(`#${dynamicTableFixedHeaderDivId} thead tr`); this.fixedTableHeader = document.querySelector(`#${dynamicTableFixedHeaderDivId} thead tr`);
this.hiddenTableHeader = this.dynamicTableDiv.querySelector("thead tr"); this.hiddenTableHeader = this.dynamicTableDiv.querySelector("thead tr");
this.table = this.dynamicTableDiv.querySelector("table"); this.table = this.dynamicTableDiv.querySelector("table");
@ -730,7 +731,7 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
setupAltRow() { setupAltRow() {
const useAltRowColors = (LocalPreferences.get("use_alt_row_colors", "true") === "true"); const useAltRowColors = clientData.getCached("use_alt_row_colors") !== false;
if (useAltRowColors) if (useAltRowColors)
document.getElementById(this.dynamicTableDivId).classList.add("altRowColors"); document.getElementById(this.dynamicTableDivId).classList.add("altRowColors");
} }
@ -1425,7 +1426,7 @@ window.qBittorrent.DynamicTable ??= (() => {
// added on // added on
this.columns["added_on"].updateTd = function(td, row) { this.columns["added_on"].updateTd = function(td, row) {
const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000));
td.textContent = date; td.textContent = date;
td.title = date; td.title = date;
}; };
@ -1438,7 +1439,7 @@ window.qBittorrent.DynamicTable ??= (() => {
td.title = ""; td.title = "";
} }
else { else {
const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000));
td.textContent = date; td.textContent = date;
td.title = date; td.title = date;
} }
@ -1805,7 +1806,7 @@ window.qBittorrent.DynamicTable ??= (() => {
? "dblclick_download" ? "dblclick_download"
: "dblclick_complete"; : "dblclick_complete";
if (LocalPreferences.get(prefKey, "1") !== "1") if (clientData.getCached(prefKey) === "0")
return true; return true;
if (state.includes("stopped")) if (state.includes("stopped"))
@ -1963,7 +1964,7 @@ window.qBittorrent.DynamicTable ??= (() => {
}; };
const displayDate = function(td, row) { const displayDate = function(td, row) {
const value = this.getRowValue(row) * 1000; const value = this.getRowValue(row) * 1000;
const formattedValue = (Number.isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString()); const formattedValue = (Number.isNaN(value) || (value <= 0)) ? "" : window.qBittorrent.Misc.formatDate(new Date(value));
td.textContent = formattedValue; td.textContent = formattedValue;
td.title = formattedValue; td.title = formattedValue;
}; };
@ -3733,7 +3734,7 @@ window.qBittorrent.DynamicTable ??= (() => {
initColumnsFunctions() { initColumnsFunctions() {
this.columns["timestamp"].updateTd = function(td, row) { this.columns["timestamp"].updateTd = function(td, row) {
const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000));
td.textContent = date; td.textContent = date;
td.title = date; td.title = date;
}; };
@ -3811,7 +3812,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true); this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
this.columns["timestamp"].updateTd = function(td, row) { this.columns["timestamp"].updateTd = function(td, row) {
const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); const date = window.qBittorrent.Misc.formatDate(new Date(this.getRowValue(row) * 1000));
td.textContent = date; td.textContent = date;
td.title = date; td.title = date;
}; };
@ -4035,7 +4036,7 @@ window.qBittorrent.DynamicTable ??= (() => {
td.title = ""; td.title = "";
} }
else { else {
const date = new Date(val).toLocaleString(); const date = window.qBittorrent.Misc.formatDate(new Date(val));
td.textContent = date; td.textContent = date;
td.title = date; td.title = date;
} }

View file

@ -28,6 +28,108 @@
"use strict"; "use strict";
/**
* @type Record<string, {locale: string, options: {}}>
*/
const DateFormatOptions = {
"MM/dd/yyyy, h:mm:ss AM/PM": {
locale: "en-US",
options: {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true
}
},
"MM/dd/yyyy, HH:mm:ss": {
locale: "en-US",
options: {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}
},
"dd/MM/yyyy, HH:mm:ss": {
locale: "en-GB",
options: {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}
},
"yyyy-MM-dd HH:mm:ss": {
locale: "sv-SE",
options: {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}
},
"yyyy/MM/dd HH:mm:ss": {
locale: "ja-JP",
options: {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}
},
"dd.MM.yyyy, HH:mm:ss": {
locale: "en-GB",
options: {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}
},
"MMM dd, yyyy, h:mm:ss AM/PM": {
locale: "en-US",
options: {
year: "numeric",
month: "short",
day: "2-digit",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true
}
},
"dd MMM yyyy, HH:mm:ss": {
locale: "en-GB",
options: {
year: "numeric",
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}
},
};
window.qBittorrent ??= {}; window.qBittorrent ??= {};
window.qBittorrent.Misc ??= (() => { window.qBittorrent.Misc ??= (() => {
const exports = () => { const exports = () => {
@ -46,6 +148,7 @@ window.qBittorrent.Misc ??= (() => {
containsAllTerms: containsAllTerms, containsAllTerms: containsAllTerms,
sleep: sleep, sleep: sleep,
downloadFile: downloadFile, downloadFile: downloadFile,
formatDate: formatDate,
// variables // variables
FILTER_INPUT_DELAY: 400, FILTER_INPUT_DELAY: 400,
MAX_ETA: 8640000 MAX_ETA: 8640000
@ -302,6 +405,21 @@ window.qBittorrent.Misc ??= (() => {
} }
}; };
/**
* @param {Date} date
* @param {string} format
* @returns {string}
*/
const formatDate = (date, format = window.parent.qBittorrent.ClientData.getCached("date_format")) => {
if ((format === "default") || !Object.keys(DateFormatOptions).includes(format))
return date.toLocaleString();
const { locale, options } = DateFormatOptions[format];
const formatter = new Intl.DateTimeFormat(locale, options);
const formatted = formatter.format(date).replace(" at ", ", ");
return format.includes(".") ? formatted.replaceAll("/", ".") : formatted;
};
return exports(); return exports();
})(); })();
Object.freeze(window.qBittorrent.Misc); Object.freeze(window.qBittorrent.Misc);

View file

@ -177,7 +177,7 @@ window.qBittorrent.PropGeneral ??= (() => {
document.getElementById("reannounce").textContent = window.qBittorrent.Misc.friendlyDuration(data.reannounce); document.getElementById("reannounce").textContent = window.qBittorrent.Misc.friendlyDuration(data.reannounce);
const lastSeen = (data.last_seen >= 0) const lastSeen = (data.last_seen >= 0)
? new Date(data.last_seen * 1000).toLocaleString() ? window.qBittorrent.Misc.formatDate(new Date(data.last_seen * 1000))
: "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]"; : "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]";
document.getElementById("last_seen").textContent = lastSeen; document.getElementById("last_seen").textContent = lastSeen;
@ -195,17 +195,17 @@ window.qBittorrent.PropGeneral ??= (() => {
document.getElementById("created_by").textContent = data.created_by; document.getElementById("created_by").textContent = data.created_by;
const additionDate = (data.addition_date >= 0) const additionDate = (data.addition_date >= 0)
? new Date(data.addition_date * 1000).toLocaleString() ? window.qBittorrent.Misc.formatDate(new Date(data.addition_date * 1000))
: "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; : "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
document.getElementById("addition_date").textContent = additionDate; document.getElementById("addition_date").textContent = additionDate;
const completionDate = (data.completion_date >= 0) const completionDate = (data.completion_date >= 0)
? new Date(data.completion_date * 1000).toLocaleString() ? window.qBittorrent.Misc.formatDate(new Date(data.completion_date * 1000))
: ""; : "";
document.getElementById("completion_date").textContent = completionDate; document.getElementById("completion_date").textContent = completionDate;
const creationDate = (data.creation_date >= 0) const creationDate = (data.creation_date >= 0)
? new Date(data.creation_date * 1000).toLocaleString() ? window.qBittorrent.Misc.formatDate(new Date(data.creation_date * 1000))
: ""; : "";
document.getElementById("creation_date").textContent = creationDate; document.getElementById("creation_date").textContent = creationDate;

View file

@ -107,8 +107,7 @@ window.qBittorrent.Search ??= (() => {
}); });
const init = () => { const init = () => {
// load "Search in" preference from local storage document.getElementById("searchInTorrentName").value = (window.qBittorrent.ClientData.getCached("search_in_filter") === "names") ? "names" : "everywhere";
document.getElementById("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere";
const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: "#searchResultsTableDiv tbody tr", targets: "#searchResultsTableDiv tbody tr",
menu: "searchResultsTableMenu", menu: "searchResultsTableMenu",
@ -804,7 +803,8 @@ window.qBittorrent.Search ??= (() => {
}; };
const searchInTorrentName = () => { const searchInTorrentName = () => {
LocalPreferences.set("search_in_filter", getSearchInTorrentName()); // don't await this
window.qBittorrent.ClientData.set({ search_in_filter: getSearchInTorrentName() });
searchFilterChanged(); searchFilterChanged();
}; };

View file

@ -58,13 +58,10 @@
// Set current "Delete files" choice as the default // Set current "Delete files" choice as the default
rememberButton.addEventListener("click", (e) => { rememberButton.addEventListener("click", (e) => {
window.qBittorrent.Cache.preferences.set({ window.qBittorrent.Cache.preferences.set({
data: {
delete_torrent_content_files: deleteCB.checked delete_torrent_content_files: deleteCB.checked
}, }).then(() => {
onSuccess: () => {
prefDeleteContentFiles = deleteCB.checked; prefDeleteContentFiles = deleteCB.checked;
setRememberBtnEnabled(false); setRememberBtnEnabled(false);
}
}); });
}); });

View file

@ -138,7 +138,7 @@
return option; return option;
}; };
const init = () => { const init = async () => {
const pieceSizeSelect = document.getElementById("pieceSize"); const pieceSizeSelect = document.getElementById("pieceSize");
pieceSizeSelect.appendChild(createSizeOption(0)); pieceSizeSelect.appendChild(createSizeOption(0));
for (let i = 4; i <= 17; ++i) for (let i = 4; i <= 17; ++i)
@ -163,7 +163,7 @@
submit(); submit();
}); });
loadPreference(); await loadPreference();
window.qBittorrent.pathAutofill.attachPathAutofill(); window.qBittorrent.pathAutofill.attachPathAutofill();
}; };
@ -207,11 +207,13 @@
comments: document.getElementById("comments").value, comments: document.getElementById("comments").value,
source: document.getElementById("source").value, source: document.getElementById("source").value,
}; };
LocalPreferences.set("torrent_creator", JSON.stringify(preference)); window.parent.qBittorrent.ClientData.set({ torrent_creator: preference });
}; };
const loadPreference = () => { const loadPreference = async () => {
const preference = JSON.parse(LocalPreferences.get("torrent_creator") ?? "{}"); const clientData = await window.parent.qBittorrent.ClientData.get(["torrent_creator"]);
const preference = clientData.torrent_creator ?? {};
document.getElementById("sourcePath").value = preference.sourcePath ?? ""; document.getElementById("sourcePath").value = preference.sourcePath ?? "";
document.getElementById("torrentFormat").value = preference.torrentFormat ?? "hybrid"; document.getElementById("torrentFormat").value = preference.torrentFormat ?? "hybrid";
document.getElementById("pieceSize").value = preference.pieceSize ?? 0; document.getElementById("pieceSize").value = preference.pieceSize ?? 0;

View file

@ -261,7 +261,7 @@
}); });
document.getElementById("Filters_pad").addEventListener("dblclick", (event) => { document.getElementById("Filters_pad").addEventListener("dblclick", (event) => {
if (LocalPreferences.get("dblclick_filter", "1") !== "1") if (window.qBittorrent.ClientData.getCached("dblclick_filter") === "0")
return; return;
const filterItem = event.target.closest("li"); const filterItem = event.target.closest("li");

View file

@ -185,7 +185,7 @@
let logFilterTimer = -1; let logFilterTimer = -1;
let inputtedFilterText = ""; let inputtedFilterText = "";
let selectBox; let selectBox;
let selectedLogLevels = JSON.parse(LocalPreferences.get("qbt_selected_log_levels")) || ["1", "2", "4", "8"]; let selectedLogLevels = window.qBittorrent.ClientData.getCached("qbt_selected_log_levels") ?? ["1", "2", "4", "8"];
const init = () => { const init = () => {
for (const option of document.getElementById("logLevelSelect").options) for (const option of document.getElementById("logLevelSelect").options)
@ -276,7 +276,8 @@
if (selectedLogLevels !== value) { if (selectedLogLevels !== value) {
tableInfo[currentSelectedTab].last_id = -1; tableInfo[currentSelectedTab].last_id = -1;
selectedLogLevels = value; selectedLogLevels = value;
LocalPreferences.set("qbt_selected_log_levels", JSON.stringify(selectedLogLevels)); // don't await this
window.qBittorrent.ClientData.set({ qbt_selected_log_levels: selectedLogLevels });
logFilterChanged(); logFilterChanged();
} }
}; };

View file

@ -1,10 +1,34 @@
<div id="BehaviorTab" class="PrefTab"> <div id="BehaviorTab" class="PrefTab">
<fieldset class="settings"> <fieldset class="settings">
<legend>QBT_TR(Language)QBT_TR[CONTEXT=OptionsDialog]</legend> <legend>QBT_TR(Localization)QBT_TR[CONTEXT=OptionsDialog]</legend>
<label for="locale_select">QBT_TR(User interface language:)QBT_TR[CONTEXT=OptionsDialog]</label> <table>
<tbody>
<tr>
<td><label for="locale_select">QBT_TR(User interface language:)QBT_TR[CONTEXT=OptionsDialog]</label></td>
<td>
<select id="locale_select"> <select id="locale_select">
${LANGUAGE_OPTIONS} ${LANGUAGE_OPTIONS}
</select> </select>
</td>
</tr>
<tr>
<td><label for="dateFormatSelect">QBT_TR(Date format:)QBT_TR[CONTEXT=OptionsDialog]</label></td>
<td>
<select id="dateFormatSelect">
<option value="default">QBT_TR(Browser default)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="MM/dd/yyyy, h:mm:ss AM/PM">QBT_TR(08/23/2025, 10:32:46 PM)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="MM/dd/yyyy, HH:mm:ss">QBT_TR(08/23/2025, 22:32:46)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="dd/MM/yyyy, HH:mm:ss">QBT_TR(23/08/2025, 22:32:46)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="yyyy-MM-dd HH:mm:ss">QBT_TR(2025-08-23 22:32:46)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="yyyy/MM/dd HH:mm:ss">QBT_TR(2025/08/23 22:32:46)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="dd.MM.yyyy, HH:mm:ss">QBT_TR(23.08.2025, 22:32:46)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="MMM dd, yyyy, h:mm:ss AM/PM">QBT_TR(Aug 23, 2025, 10:32:46 PM)QBT_TR[CONTEXT=OptionsDialog]</option>
<option value="dd MMM yyyy, HH:mm:ss">QBT_TR(23 Aug 2025, 22:32:46)QBT_TR[CONTEXT=OptionsDialog]</option>
</select>
</td>
</tr>
</tbody>
</table>
</fieldset> </fieldset>
<fieldset class="settings"> <fieldset class="settings">
@ -2215,10 +2239,8 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
document.getElementById("locale_select").value = selected; document.getElementById("locale_select").value = selected;
}; };
const updateColoSchemeSelect = () => { const updateColoSchemeSelect = (colorScheme) => {
const combobox = document.getElementById("colorSchemeSelect"); const combobox = document.getElementById("colorSchemeSelect");
const colorScheme = LocalPreferences.get("color_scheme");
if (colorScheme === "light") if (colorScheme === "light")
combobox.options[1].selected = true; combobox.options[1].selected = true;
else if (colorScheme === "dark") else if (colorScheme === "dark")
@ -2228,22 +2250,39 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
}; };
const loadPreferences = () => { const loadPreferences = () => {
window.parent.qBittorrent.Cache.preferences.init({ window.parent.qBittorrent.Cache.preferences.init()
onSuccess: (pref) => { .then((pref) => {
const clientData = window.parent.qBittorrent.ClientData;
const dateFormat = clientData.getCached("date_format");
const colorScheme = clientData.getCached("color_scheme");
const fullUrlTrackerColumn = clientData.getCached("full_url_tracker_column");
const useVirtualList = clientData.getCached("use_virtual_list");
const hideZeroStatusFilters = clientData.getCached("hide_zero_status_filters");
const dblclickDownload = clientData.getCached("dblclick_download");
const dblclickComplete = clientData.getCached("dblclick_complete");
const dblclickFilter = clientData.getCached("dblclick_filter");
const useAltRowColors = clientData.getCached("use_alt_row_colors");
// Behavior tab // Behavior tab
// Language // Localization
updateWebuiLocaleSelect(pref.locale); updateWebuiLocaleSelect(pref.locale);
updateColoSchemeSelect(); const dateFormatSelect = document.getElementById("dateFormatSelect");
if ((dateFormat?.length > 0) && ([...dateFormatSelect.options].find(o => o.value === dateFormat) !== undefined))
dateFormatSelect.value = dateFormat;
// Interface
updateColoSchemeSelect(colorScheme);
document.getElementById("statusBarExternalIP").checked = pref.status_bar_external_ip; document.getElementById("statusBarExternalIP").checked = pref.status_bar_external_ip;
document.getElementById("performanceWarning").checked = pref.performance_warning; document.getElementById("performanceWarning").checked = pref.performance_warning;
document.getElementById("displayFullURLTrackerColumn").checked = (LocalPreferences.get("full_url_tracker_column", "false") === "true"); document.getElementById("displayFullURLTrackerColumn").checked = fullUrlTrackerColumn === true;
document.getElementById("useVirtualList").checked = (LocalPreferences.get("use_virtual_list", "false") === "true"); document.getElementById("useVirtualList").checked = useVirtualList === true;
document.getElementById("hideZeroFiltersCheckbox").checked = (LocalPreferences.get("hide_zero_status_filters", "false") === "true"); document.getElementById("hideZeroFiltersCheckbox").checked = hideZeroStatusFilters === true;
document.getElementById("dblclickDownloadSelect").value = LocalPreferences.get("dblclick_download", "1"); document.getElementById("dblclickDownloadSelect").value = dblclickDownload ?? "1";
document.getElementById("dblclickCompleteSelect").value = LocalPreferences.get("dblclick_complete", "1"); document.getElementById("dblclickCompleteSelect").value = dblclickComplete ?? "1";
document.getElementById("dblclickFiltersSelect").value = LocalPreferences.get("dblclick_filter", "1"); document.getElementById("dblclickFiltersSelect").value = dblclickFilter ?? "1";
document.getElementById("confirmTorrentDeletion").checked = pref.confirm_torrent_deletion; document.getElementById("confirmTorrentDeletion").checked = pref.confirm_torrent_deletion;
document.getElementById("useAltRowColorsInput").checked = (LocalPreferences.get("use_alt_row_colors", "true") === "true"); document.getElementById("useAltRowColorsInput").checked = useAltRowColors !== false;
document.getElementById("filelog_checkbox").checked = pref.file_log_enabled; document.getElementById("filelog_checkbox").checked = pref.file_log_enabled;
document.getElementById("filelog_save_path_input").value = pref.file_log_path; document.getElementById("filelog_save_path_input").value = pref.file_log_path;
document.getElementById("filelog_backup_checkbox").checked = pref.file_log_backup_enabled; document.getElementById("filelog_backup_checkbox").checked = pref.file_log_backup_enabled;
@ -2650,34 +2689,38 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
document.getElementById("i2pOutboundQuantity").value = pref.i2p_outbound_quantity; document.getElementById("i2pOutboundQuantity").value = pref.i2p_outbound_quantity;
document.getElementById("i2pInboundLength").value = pref.i2p_inbound_length; document.getElementById("i2pInboundLength").value = pref.i2p_inbound_length;
document.getElementById("i2pOutboundLength").value = pref.i2p_outbound_length; document.getElementById("i2pOutboundLength").value = pref.i2p_outbound_length;
} }).catch((error) => {
console.error(error);
alert("QBT_TR(Unable to load program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
}); });
}; };
const applyPreferences = () => { const applyPreferences = () => {
const settings = {}; const settings = {};
const clientData = {};
// Validate form data // Validate form data
// Behavior tab // Behavior tab
// Language // Language
settings["locale"] = document.getElementById("locale_select").value; settings["locale"] = document.getElementById("locale_select").value;
clientData.date_format = document.getElementById("dateFormatSelect").value;
const colorScheme = Number(document.getElementById("colorSchemeSelect").value); const colorScheme = Number(document.getElementById("colorSchemeSelect").value);
if (colorScheme === 0) if (colorScheme === 0)
LocalPreferences.remove("color_scheme"); clientData.color_scheme = null;
else if (colorScheme === 1) else if (colorScheme === 1)
LocalPreferences.set("color_scheme", "light"); clientData.color_scheme = "light";
else else
LocalPreferences.set("color_scheme", "dark"); clientData.color_scheme = "dark";
settings["status_bar_external_ip"] = document.getElementById("statusBarExternalIP").checked; settings["status_bar_external_ip"] = document.getElementById("statusBarExternalIP").checked;
settings["performance_warning"] = document.getElementById("performanceWarning").checked; settings["performance_warning"] = document.getElementById("performanceWarning").checked;
LocalPreferences.set("full_url_tracker_column", document.getElementById("displayFullURLTrackerColumn").checked.toString()); clientData.full_url_tracker_column = document.getElementById("displayFullURLTrackerColumn").checked;
LocalPreferences.set("use_virtual_list", document.getElementById("useVirtualList").checked.toString()); clientData.use_virtual_list = document.getElementById("useVirtualList").checked;
LocalPreferences.set("hide_zero_status_filters", document.getElementById("hideZeroFiltersCheckbox").checked.toString()); clientData.hide_zero_status_filters = document.getElementById("hideZeroFiltersCheckbox").checked;
LocalPreferences.set("dblclick_download", document.getElementById("dblclickDownloadSelect").value); clientData.dblclick_download = document.getElementById("dblclickDownloadSelect").value;
LocalPreferences.set("dblclick_complete", document.getElementById("dblclickCompleteSelect").value); clientData.dblclick_complete = document.getElementById("dblclickCompleteSelect").value;
LocalPreferences.set("dblclick_filter", document.getElementById("dblclickFiltersSelect").value); clientData.dblclick_filter = document.getElementById("dblclickFiltersSelect").value;
settings["confirm_torrent_deletion"] = document.getElementById("confirmTorrentDeletion").checked; settings["confirm_torrent_deletion"] = document.getElementById("confirmTorrentDeletion").checked;
LocalPreferences.set("use_alt_row_colors", document.getElementById("useAltRowColorsInput").checked.toString()); clientData.use_alt_row_colors = document.getElementById("useAltRowColorsInput").checked;
settings["file_log_enabled"] = document.getElementById("filelog_checkbox").checked; settings["file_log_enabled"] = document.getElementById("filelog_checkbox").checked;
settings["file_log_path"] = document.getElementById("filelog_save_path_input").value; settings["file_log_path"] = document.getElementById("filelog_save_path_input").value;
settings["file_log_backup_enabled"] = document.getElementById("filelog_backup_checkbox").checked; settings["file_log_backup_enabled"] = document.getElementById("filelog_backup_checkbox").checked;
@ -3168,18 +3211,20 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
settings["i2p_inbound_length"] = Number(document.getElementById("i2pInboundLength").value); settings["i2p_inbound_length"] = Number(document.getElementById("i2pInboundLength").value);
settings["i2p_outbound_length"] = Number(document.getElementById("i2pOutboundLength").value); settings["i2p_outbound_length"] = Number(document.getElementById("i2pOutboundLength").value);
window.parent.qBittorrent.ClientData.set(clientData)
.catch((error) => {
alert("QBT_TR(Unable to save web client preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
});
// Send it to qBT // Send it to qBT
window.parent.qBittorrent.Cache.preferences.set({ window.parent.qBittorrent.Cache.preferences.set(settings)
data: settings, .then(() => {
onFailure: () => {
alert("QBT_TR(Unable to save program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage"));
},
onSuccess: () => {
// Close window // Close window
window.parent.location.reload(); window.parent.location.reload();
window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage")); window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage"));
} }).catch((error) => {
alert("QBT_TR(Unable to save program preferences, qBittorrent is probably unreachable.)QBT_TR[CONTEXT=HttpServer]");
window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage"));
}); });
}; };

View file

@ -456,7 +456,7 @@
torrentDate.append(torrentDateDesc); torrentDate.append(torrentDateDesc);
const torrentDateData = document.createElement("span"); const torrentDateData = document.createElement("span");
torrentDateData.textContent = new Date(articleDate).toLocaleString(); torrentDateData.textContent = window.qBittorrent.Misc.formatDate(new Date(articleDate));
torrentDate.append(torrentDateData); torrentDate.append(torrentDateData);
detailsView.append(torrentDate); detailsView.append(torrentDate);

View file

@ -26,7 +26,7 @@
* exception statement from your version. * exception statement from your version.
*/ */
import { expect, test } from "vitest"; import { vi, expect, test } from "vitest";
import "../../private/scripts/misc.js"; import "../../private/scripts/misc.js";
test("Test toFixedPointString()", () => { test("Test toFixedPointString()", () => {
@ -76,3 +76,57 @@ test("Test toFixedPointString()", () => {
expect(toFixedPointString(-100.00, 1)).toBe("-100.0"); expect(toFixedPointString(-100.00, 1)).toBe("-100.0");
expect(toFixedPointString(-100.00, 2)).toBe("-100.00"); expect(toFixedPointString(-100.00, 2)).toBe("-100.00");
}); });
test("Test formatDate() - Format Coverage", () => {
const formatDate = window.qBittorrent.Misc.formatDate;
const testDate = new Date(2025, 7, 23, 22, 32, 46); // Aug 23, 2025 10:32:46 PM
expect(formatDate(testDate, "MM/dd/yyyy, h:mm:ss AM/PM")).toBe("08/23/2025, 10:32:46 PM");
expect(formatDate(testDate, "MM/dd/yyyy, HH:mm:ss")).toBe("08/23/2025, 22:32:46");
expect(formatDate(testDate, "dd/MM/yyyy, HH:mm:ss")).toBe("23/08/2025, 22:32:46");
expect(formatDate(testDate, "yyyy-MM-dd HH:mm:ss")).toBe("2025-08-23 22:32:46");
expect(formatDate(testDate, "yyyy/MM/dd HH:mm:ss")).toBe("2025/08/23 22:32:46");
expect(formatDate(testDate, "dd.MM.yyyy, HH:mm:ss")).toBe("23.08.2025, 22:32:46");
expect(formatDate(testDate, "MMM dd, yyyy, h:mm:ss AM/PM")).toBe("Aug 23, 2025, 10:32:46 PM");
expect(formatDate(testDate, "dd MMM yyyy, HH:mm:ss")).toBe("23 Aug 2025, 22:32:46");
});
test("Test formatDate() - Fallback Behavior", () => {
// Mock ClientData.getCached
const mockGetCached = vi.fn().mockReturnValue("default");
const originalParent = window.parent;
window.parent = {
qBittorrent: {
ClientData: {
getCached: mockGetCached
}
}
};
const formatDate = window.qBittorrent.Misc.formatDate;
const testDate = new Date(2025, 7, 23, 22, 32, 46); // Aug 23, 2025 10:32:46 PM
const expectedDefault = testDate.toLocaleString();
// Test that "default" format uses toLocaleString()
expect(formatDate(testDate, "default")).toBe(expectedDefault);
// Test default behavior when no format argument is provided
expect(mockGetCached).toHaveBeenCalledTimes(0);
expect(formatDate(testDate)).toBe(expectedDefault);
expect(mockGetCached).toHaveBeenCalledWith("date_format");
expect(mockGetCached).toHaveBeenCalledTimes(1);
// Test with unknown/invalid format strings
expect(formatDate(testDate, "invalid-format")).toBe(expectedDefault);
expect(formatDate(testDate, "")).toBe(expectedDefault);
expect(formatDate(testDate, null)).toBe(expectedDefault);
expect(mockGetCached).toHaveBeenCalledTimes(1);
expect(formatDate(testDate, undefined)).toBe(expectedDefault);
expect(mockGetCached).toHaveBeenCalledWith("date_format");
expect(mockGetCached).toHaveBeenCalledTimes(2);
// Restore original window.parent
window.parent = originalParent;
});

View file

@ -390,6 +390,7 @@
<file>private/scripts/addtorrent.js</file> <file>private/scripts/addtorrent.js</file>
<file>private/scripts/cache.js</file> <file>private/scripts/cache.js</file>
<file>private/scripts/client.js</file> <file>private/scripts/client.js</file>
<file>private/scripts/client-data.js</file>
<file>private/scripts/color-scheme.js</file> <file>private/scripts/color-scheme.js</file>
<file>private/scripts/contextmenu.js</file> <file>private/scripts/contextmenu.js</file>
<file>private/scripts/dynamicTable.js</file> <file>private/scripts/dynamicTable.js</file>