Skip to content

Commit 77c79d1

Browse files
committed
WebUI: Store durable settings in client data API
1 parent 7ddbf58 commit 77c79d1

File tree

12 files changed

+272
-88
lines changed

12 files changed

+272
-88
lines changed

src/webui/www/private/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<script defer src="scripts/monkeypatch.js?v=${CACHEID}"></script>
3030
<script defer src="scripts/cache.js?v=${CACHEID}"></script>
3131
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
32+
<script defer src="scripts/client-data.js?v=${CACHEID}"></script>
3233
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
3334
<script defer src="scripts/mocha-init.js?locale=${LANG}&v=${CACHEID}"></script>
3435
<script defer src="scripts/lib/clipboard-copy.js"></script>

src/webui/www/private/scripts/addtorrent.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ window.qBittorrent.AddTorrent ??= (() => {
4444
let source = "";
4545
let downloader = "";
4646

47-
const localPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
47+
const clientData = window.parent.qBittorrent.ClientData;
4848

4949
const getCategories = () => {
50-
const defaultCategory = localPreferences.get("add_torrent_default_category", "");
50+
const defaultCategory = clientData.get("add_torrent_default_category") ?? "";
5151
const categorySelect = document.getElementById("categorySelect");
5252
for (const name of window.parent.qBittorrent.Client.categoryMap.keys()) {
5353
const option = document.createElement("option");
@@ -319,10 +319,7 @@ window.qBittorrent.AddTorrent ??= (() => {
319319

320320
if (document.getElementById("setDefaultCategory").checked) {
321321
const category = document.getElementById("category").value.trim();
322-
if (category.length === 0)
323-
localPreferences.remove("add_torrent_default_category");
324-
else
325-
localPreferences.set("add_torrent_default_category", category);
322+
clientData.set({ add_torrent_default_category: (category.length > 0) ? category : null });
326323
}
327324
};
328325

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Bittorrent Client using Qt and libtorrent.
3+
* Copyright (C) 2025 Thomas Piccirello <[email protected]>
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU General Public License
7+
* as published by the Free Software Foundation; either version 2
8+
* of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18+
*
19+
* In addition, as a special exception, the copyright holders give permission to
20+
* link this program with the OpenSSL project's "OpenSSL" library (or with
21+
* modified versions of it that use the same license as the "OpenSSL" library),
22+
* and distribute the linked executables. You must obey the GNU General Public
23+
* License in all respects for all of the code used other than "OpenSSL". If you
24+
* modify file(s), you may extend this exception to your version of the file(s),
25+
* but you are not obligated to do so. If you do not wish to do so, delete this
26+
* exception statement from your version.
27+
*/
28+
29+
"use strict";
30+
31+
window.qBittorrent ??= {};
32+
window.qBittorrent.ClientData ??= (() => {
33+
const exports = () => {
34+
return new ClientData();
35+
};
36+
37+
// this is exposed as a singleton
38+
class ClientData {
39+
/**
40+
* @type Map<string, any>
41+
*/
42+
#cache = new Map();
43+
44+
#keyPrefix = "qbt_";
45+
46+
#addKeyPrefix(data) {
47+
return Object.fromEntries(Object.entries(data).map(([key, value]) => ([`${this.#keyPrefix}${key}`, value])));
48+
}
49+
50+
#removeKeyPrefix(data) {
51+
return Object.fromEntries(Object.entries(data).map(([key, value]) => ([key.substring(this.#keyPrefix.length), value])));
52+
}
53+
54+
/**
55+
* @param {string[]} keys
56+
* @returns {Record<string, any>}
57+
*/
58+
async #fetch(keys) {
59+
keys = keys.map(key => `${this.#keyPrefix}${key}`);
60+
return await fetch("api/v2/clientdata/load", {
61+
method: "POST",
62+
body: new URLSearchParams({
63+
keys: JSON.stringify(keys)
64+
})
65+
})
66+
.then(async (response) => {
67+
if (!response.ok)
68+
return;
69+
70+
const data = await response.json();
71+
return this.#removeKeyPrefix(data);
72+
});
73+
}
74+
75+
/**
76+
* @param {Record<string, any>} data
77+
*/
78+
async #set(data) {
79+
data = this.#addKeyPrefix(data);
80+
await fetch("api/v2/clientdata/store", {
81+
method: "POST",
82+
body: new URLSearchParams({
83+
data: JSON.stringify(data)
84+
})
85+
})
86+
.then((response) => {
87+
if (!response.ok)
88+
throw new Error("Failed to store client data");
89+
});
90+
}
91+
92+
/**
93+
* @param {string} key
94+
* @returns {any}
95+
*/
96+
get(key) {
97+
return this.#cache.get(key);
98+
}
99+
100+
/**
101+
* @param {string[]} keys
102+
* @returns {Record<string, any>}
103+
*/
104+
async fetch(keys = []) {
105+
const keysToFetch = keys.filter((key) => !this.#cache.has(key));
106+
if (keysToFetch.length > 0) {
107+
const fetchedData = await this.#fetch(keysToFetch);
108+
for (const [key, value] of Object.entries(fetchedData))
109+
this.#cache.set(key, value);
110+
}
111+
112+
return Object.fromEntries(keys.map((key) => ([key, this.#cache.get(key)])));
113+
}
114+
115+
/**
116+
* @param {Record<string, any>} data
117+
*/
118+
async set(data) {
119+
try {
120+
await this.#set(data);
121+
}
122+
catch (err) {
123+
console.error(err);
124+
return;
125+
}
126+
127+
// update cache
128+
for (const [key, value] of Object.entries(data))
129+
this.#cache.set(key, value);
130+
}
131+
}
132+
133+
return exports();
134+
})();
135+
Object.freeze(window.qBittorrent.ClientData);

src/webui/www/private/scripts/client.js

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ window.qBittorrent.Client ??= (() => {
3131
return {
3232
setup: setup,
3333
initializeCaches: initializeCaches,
34+
initializeClientData: initializeClientData,
3435
closeWindow: closeWindow,
3536
closeFrameWindow: closeFrameWindow,
3637
getSyncMainDataInterval: getSyncMainDataInterval,
@@ -56,15 +57,47 @@ window.qBittorrent.Client ??= (() => {
5657
const tagMap = new Map();
5758

5859
let cacheAllSettled;
60+
let clientDataPromise;
5961
const setup = () => {
6062
// fetch various data and store it in memory
63+
clientDataPromise = window.qBittorrent.ClientData.fetch([
64+
"show_search_engine",
65+
"show_rss_reader",
66+
"show_log_viewer",
67+
"speed_in_browser_title_bar",
68+
"show_top_toolbar",
69+
"show_status_bar",
70+
"show_filters_sidebar",
71+
"hide_zero_status_filters",
72+
"color_scheme",
73+
"full_url_tracker_column",
74+
"use_alt_row_colors",
75+
"use_virtual_list",
76+
"dblclick_complete",
77+
"dblclick_download",
78+
"dblclick_filter",
79+
"search_in_filter",
80+
"qbt_selected_log_levels",
81+
"add_torrent_default_category",
82+
]);
83+
6184
cacheAllSettled = Promise.allSettled([
6285
window.qBittorrent.Cache.buildInfo.init(),
6386
window.qBittorrent.Cache.preferences.init(),
64-
window.qBittorrent.Cache.qbtVersion.init()
87+
window.qBittorrent.Cache.qbtVersion.init(),
88+
clientDataPromise,
6589
]);
6690
};
6791

92+
const initializeClientData = async () => {
93+
try {
94+
await clientDataPromise;
95+
}
96+
catch (error) {
97+
console.error(`Failed to initialize client data. Reason: "${error}".`);
98+
}
99+
};
100+
68101
const initializeCaches = async () => {
69102
const results = await cacheAllSettled;
70103
for (const [idx, result] of results.entries()) {
@@ -223,8 +256,8 @@ let queueing_enabled = true;
223256
let serverSyncMainDataInterval = 1500;
224257
let customSyncMainDataInterval = null;
225258
let useSubcategories = true;
226-
const useAutoHideZeroStatusFilters = localPreferences.get("hide_zero_status_filters", "false") === "true";
227-
const displayFullURLTrackerColumn = localPreferences.get("full_url_tracker_column", "false") === "true";
259+
let useAutoHideZeroStatusFilters = false;
260+
let displayFullURLTrackerColumn = false;
228261

229262
/* Categories filter */
230263
const CATEGORIES_ALL = "b4af0e4c-e76d-4bac-a392-46cbc18d9655";
@@ -250,6 +283,8 @@ const TRACKERS_WARNING = "82a702c5-210c-412b-829f-97632d7557e9";
250283
// Map<trackerHost: String, Map<trackerURL: String, torrents: Set>>
251284
const trackerMap = new Map();
252285

286+
const clientData = window.qBittorrent.ClientData;
287+
253288
let selectedTracker = localPreferences.get("selected_tracker", TRACKERS_ALL);
254289
let setTrackerFilter = () => {};
255290

@@ -258,7 +293,13 @@ let selectedStatus = localPreferences.get("selected_filter", "all");
258293
let setStatusFilter = () => {};
259294
let toggleFilterDisplay = () => {};
260295

261-
window.addEventListener("DOMContentLoaded", (event) => {
296+
window.addEventListener("DOMContentLoaded", async (event) => {
297+
await window.qBittorrent.Client.initializeClientData();
298+
window.qBittorrent.ColorScheme.update();
299+
300+
useAutoHideZeroStatusFilters = clientData.get("hide_zero_status_filters") === true;
301+
displayFullURLTrackerColumn = clientData.get("full_url_tracker_column") === true;
302+
262303
window.qBittorrent.LocalPreferences.upgrade();
263304

264305
let isSearchPanelLoaded = false;
@@ -405,6 +446,13 @@ window.addEventListener("DOMContentLoaded", (event) => {
405446
localPreferences.set(`filter_${filterListID.replace("FilterList", "")}_collapsed`, filterList.classList.toggle("invisible").toString());
406447
};
407448

449+
const highlightSelectedStatus = () => {
450+
const statusFilter = document.getElementById("statusFilterList");
451+
const filterID = `${selectedStatus}_filter`;
452+
for (const status of statusFilter.children)
453+
status.classList.toggle("selectedFilter", (status.id === filterID));
454+
};
455+
408456
new MochaUI.Panel({
409457
id: "Filters",
410458
title: "Panel",
@@ -426,35 +474,35 @@ window.addEventListener("DOMContentLoaded", (event) => {
426474
initializeWindows();
427475

428476
// Show Top Toolbar is enabled by default
429-
let showTopToolbar = localPreferences.get("show_top_toolbar", "true") === "true";
477+
let showTopToolbar = clientData.get("show_top_toolbar") !== false;
430478
if (!showTopToolbar) {
431479
document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0";
432480
document.getElementById("mochaToolbar").classList.add("invisible");
433481
}
434482

435483
// Show Status Bar is enabled by default
436-
let showStatusBar = localPreferences.get("show_status_bar", "true") === "true";
484+
let showStatusBar = clientData.get("show_status_bar") !== false;
437485
if (!showStatusBar) {
438486
document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0";
439487
document.getElementById("desktopFooterWrapper").classList.add("invisible");
440488
}
441489

442490
// Show Filters Sidebar is enabled by default
443-
let showFiltersSidebar = localPreferences.get("show_filters_sidebar", "true") === "true";
491+
let showFiltersSidebar = clientData.get("show_filters_sidebar") !== false;
444492
if (!showFiltersSidebar) {
445493
document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0";
446494
document.getElementById("filtersColumn").classList.add("invisible");
447495
document.getElementById("filtersColumn_handle").classList.add("invisible");
448496
}
449497

450-
let speedInTitle = localPreferences.get("speed_in_browser_title_bar") === "true";
498+
let speedInTitle = clientData.get("speed_in_browser_title_bar") === true;
451499
if (!speedInTitle)
452500
document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0";
453501

454502
// After showing/hiding the toolbar + status bar
455-
window.qBittorrent.Client.showSearchEngine(localPreferences.get("show_search_engine") !== "false");
456-
window.qBittorrent.Client.showRssReader(localPreferences.get("show_rss_reader") !== "false");
457-
window.qBittorrent.Client.showLogViewer(localPreferences.get("show_log_viewer") === "true");
503+
window.qBittorrent.Client.showSearchEngine(clientData.get("show_search_engine") !== false);
504+
window.qBittorrent.Client.showRssReader(clientData.get("show_rss_reader") !== false);
505+
window.qBittorrent.Client.showLogViewer(clientData.get("show_log_viewer") === true);
458506

459507
// After Show Top Toolbar
460508
MochaUI.Desktop.setDesktopSize();
@@ -571,13 +619,6 @@ window.addEventListener("DOMContentLoaded", (event) => {
571619
window.qBittorrent.Filters.clearStatusFilter();
572620
};
573621

574-
const highlightSelectedStatus = () => {
575-
const statusFilter = document.getElementById("statusFilterList");
576-
const filterID = `${selectedStatus}_filter`;
577-
for (const status of statusFilter.children)
578-
status.classList.toggle("selectedFilter", (status.id === filterID));
579-
};
580-
581622
const updateCategoryList = () => {
582623
const categoryList = document.getElementById("categoryFilterList");
583624
if (!categoryList)
@@ -1223,7 +1264,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
12231264

12241265
document.getElementById("showTopToolbarLink").addEventListener("click", (e) => {
12251266
showTopToolbar = !showTopToolbar;
1226-
localPreferences.set("show_top_toolbar", showTopToolbar.toString());
1267+
clientData.set({ show_top_toolbar: showTopToolbar });
12271268
if (showTopToolbar) {
12281269
document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "1";
12291270
document.getElementById("mochaToolbar").classList.remove("invisible");
@@ -1237,7 +1278,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
12371278

12381279
document.getElementById("showStatusBarLink").addEventListener("click", (e) => {
12391280
showStatusBar = !showStatusBar;
1240-
localPreferences.set("show_status_bar", showStatusBar.toString());
1281+
clientData.set({ show_status_bar: showStatusBar });
12411282
if (showStatusBar) {
12421283
document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "1";
12431284
document.getElementById("desktopFooterWrapper").classList.remove("invisible");
@@ -1274,7 +1315,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
12741315

12751316
document.getElementById("showFiltersSidebarLink").addEventListener("click", (e) => {
12761317
showFiltersSidebar = !showFiltersSidebar;
1277-
localPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1318+
clientData.set({ show_filters_sidebar: showFiltersSidebar });
12781319
if (showFiltersSidebar) {
12791320
document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "1";
12801321
document.getElementById("filtersColumn").classList.remove("invisible");
@@ -1290,7 +1331,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
12901331

12911332
document.getElementById("speedInBrowserTitleBarLink").addEventListener("click", (e) => {
12921333
speedInTitle = !speedInTitle;
1293-
localPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1334+
clientData.set({ speed_in_browser_title_bar: speedInTitle });
12941335
if (speedInTitle)
12951336
document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "1";
12961337
else
@@ -1300,19 +1341,19 @@ window.addEventListener("DOMContentLoaded", (event) => {
13001341

13011342
document.getElementById("showSearchEngineLink").addEventListener("click", (e) => {
13021343
window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1303-
localPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1344+
clientData.set({ show_search_engine: window.qBittorrent.Client.isShowSearchEngine() });
13041345
updateTabDisplay();
13051346
});
13061347

13071348
document.getElementById("showRssReaderLink").addEventListener("click", (e) => {
13081349
window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1309-
localPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1350+
clientData.set({ show_rss_reader: window.qBittorrent.Client.isShowRssReader() });
13101351
updateTabDisplay();
13111352
});
13121353

13131354
document.getElementById("showLogViewerLink").addEventListener("click", (e) => {
13141355
window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1315-
localPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1356+
clientData.set({ show_log_viewer: window.qBittorrent.Client.isShowLogViewer() });
13161357
updateTabDisplay();
13171358
});
13181359

@@ -1369,7 +1410,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
13691410
// main window tabs
13701411

13711412
const showTransfersTab = () => {
1372-
const showFiltersSidebar = localPreferences.get("show_filters_sidebar", "true") === "true";
1413+
const showFiltersSidebar = clientData.get("show_filters_sidebar") !== false;
13731414
if (showFiltersSidebar) {
13741415
document.getElementById("filtersColumn").classList.remove("invisible");
13751416
document.getElementById("filtersColumn_handle").classList.remove("invisible");

0 commit comments

Comments
 (0)