mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-20 21:33:27 -07:00
WebUI: Support push notification
This commit is contained in:
parent
885aed7838
commit
af7a6d470d
6 changed files with 368 additions and 2 deletions
|
@ -6,8 +6,8 @@
|
||||||
"url": "https://github.com/qbittorrent/qBittorrent.git"
|
"url": "https://github.com/qbittorrent/qBittorrent.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js test/*/*.js && prettier --write **.css",
|
"format": "js-beautify -r *.mjs private/*.html private/*.js 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",
|
"lint": "eslint --cache *.mjs private/*.html private/*.js private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js test/*/*.js && stylelint --cache **/*.css && html-validate private public",
|
||||||
"test": "vitest run --dom"
|
"test": "vitest run --dom"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
<script defer src="scripts/contextmenu.js?locale=${LANG}&v=${CACHEID}"></script>
|
<script defer src="scripts/contextmenu.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||||
<script defer src="scripts/pathAutofill.js?v=${CACHEID}"></script>
|
<script defer src="scripts/pathAutofill.js?v=${CACHEID}"></script>
|
||||||
<script defer src="scripts/statistics.js?v=${CACHEID}"></script>
|
<script defer src="scripts/statistics.js?v=${CACHEID}"></script>
|
||||||
|
<script defer src="scripts/webpush.js?v=${CACHEID}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
186
src/webui/www/private/scripts/webpush.js
Normal file
186
src/webui/www/private/scripts/webpush.js
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
* Copyright (C) 2025 tehcneko
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.qBittorrent ??= {};
|
||||||
|
window.qBittorrent.WebPush ??= (() => {
|
||||||
|
const exports = () => {
|
||||||
|
return {
|
||||||
|
isSupported: isSupported,
|
||||||
|
isSubscribed: isSubscribed,
|
||||||
|
registerServiceWorker: registerServiceWorker,
|
||||||
|
sendTestNotification: sendTestNotification,
|
||||||
|
subscribe: subscribe,
|
||||||
|
unsubscribe: unsubscribe,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSupported = () => {
|
||||||
|
return (
|
||||||
|
window.isSecureContext
|
||||||
|
&& ("serviceWorker" in navigator)
|
||||||
|
&& ("PushManager" in window)
|
||||||
|
&& ("Notification" in window)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerServiceWorker = async () => {
|
||||||
|
const officialWebUIServiceWorkerScript = "/sw-webui.js";
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
let registered = false;
|
||||||
|
for (const registration of registrations) {
|
||||||
|
const isOfficialWebUI = registration.active && registration.active.scriptURL.endsWith(officialWebUIServiceWorkerScript);
|
||||||
|
if (isOfficialWebUI) {
|
||||||
|
registered = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await registration.unregister();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!registered)
|
||||||
|
await navigator.serviceWorker.register(officialWebUIServiceWorkerScript);
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlBase64ToUint8Array = (base64String) => {
|
||||||
|
const padding = "=".repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/-/g, "+")
|
||||||
|
.replace(/_/g, "/");
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i)
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
|
||||||
|
return outputArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestNotificationPermission = async () => {
|
||||||
|
if (Notification.permission === "granted")
|
||||||
|
return true;
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
return permission === "granted";
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchVapidPublicKey = async () => {
|
||||||
|
const url = new URL("api/v2/push/vapidPublicKey", window.location);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-store"
|
||||||
|
});
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error("QBT_TR(Failed to fetch VAPID public key)QBT_TR[CONTEXT=PushNotification]");
|
||||||
|
const responseJSON = await response.json();
|
||||||
|
return responseJSON["vapidPublicKey"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPushManager = async () => {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
return registration.pushManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribeToPushManager = async (vapidPublicKey) => {
|
||||||
|
const pushManager = await getPushManager();
|
||||||
|
const subscription = await pushManager.getSubscription();
|
||||||
|
if (subscription !== null)
|
||||||
|
return subscription;
|
||||||
|
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
|
||||||
|
|
||||||
|
return pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: convertedVapidKey
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribeToServer = async (subscription) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("subscription", JSON.stringify(subscription));
|
||||||
|
const url = new URL("api/v2/push/subscribe", window.location);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "post",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(await response.text());
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribeFromServer = async (subscription) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("endpoint", subscription.endpoint);
|
||||||
|
const url = new URL("api/v2/push/unsubscribe", window.location);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "post",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(await response.text());
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendTestNotification = async () => {
|
||||||
|
const url = new URL("api/v2/push/test", window.location);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-store"
|
||||||
|
});
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(await response.text());
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribe = async () => {
|
||||||
|
const permissionGranted = await requestNotificationPermission();
|
||||||
|
if (!permissionGranted)
|
||||||
|
throw new Error("QBT_TR(Notification permission denied.)QBT_TR[CONTEXT=PushNotification]");
|
||||||
|
const vapidPublicKey = await fetchVapidPublicKey();
|
||||||
|
const subscription = await subscribeToPushManager(vapidPublicKey);
|
||||||
|
await subscribeToServer(subscription);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubscribed = async () => {
|
||||||
|
const pushManager = await getPushManager();
|
||||||
|
const subscription = await pushManager.getSubscription();
|
||||||
|
return subscription !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = async () => {
|
||||||
|
const pushManager = await getPushManager();
|
||||||
|
const subscription = await pushManager.getSubscription();
|
||||||
|
if (subscription !== null) {
|
||||||
|
await subscription.unsubscribe();
|
||||||
|
await unsubscribeFromServer(subscription);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports();
|
||||||
|
})();
|
||||||
|
Object.freeze(window.qBittorrent.WebPush);
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
if (window.qBittorrent.WebPush.isSupported()) {
|
||||||
|
window.qBittorrent.WebPush.registerServiceWorker().catch((error) => {
|
||||||
|
console.error("Failed to register service worker:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
100
src/webui/www/private/sw-webui.js
Normal file
100
src/webui/www/private/sw-webui.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
* Copyright (C) 2025 tehcneko
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(self.skipWaiting());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("push", (e) => {
|
||||||
|
if (e.data === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const data = e.data.json();
|
||||||
|
if (data.event === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const event = data.event;
|
||||||
|
const payload = data.payload || {};
|
||||||
|
|
||||||
|
let notificationTitle;
|
||||||
|
let notificationBody;
|
||||||
|
switch (event) {
|
||||||
|
case "test":
|
||||||
|
notificationTitle = "QBT_TR(Test Notification)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
notificationBody = "QBT_TR(This is a test notification. Thank you for using qBittorrent.)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
break;
|
||||||
|
case "torrent_added":
|
||||||
|
// ignore for now.
|
||||||
|
return;
|
||||||
|
case "torrent_finished":
|
||||||
|
notificationTitle = "QBT_TR(Download completed)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
notificationBody = "QBT_TR(%1 has finished downloading.)QBT_TR[CONTEXT=PushNotification]"
|
||||||
|
.replace("%1", `"${payload.torrent_name}"`);
|
||||||
|
break;
|
||||||
|
case "full_disk_error":
|
||||||
|
notificationTitle = "QBT_TR(I/O Error)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
notificationBody = "QBT_TR(An I/O error occurred for torrent %1.\n Reason: %2)QBT_TR[CONTEXT=PushNotification]"
|
||||||
|
.replace("%1", `"${payload.torrent_name}"`)
|
||||||
|
.replace("%2", payload.reason);
|
||||||
|
break;
|
||||||
|
case "add_torrent_failed":
|
||||||
|
notificationTitle = "QBT_TR(Add torrent failed)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
notificationBody = "QBT_TR(Couldn't add torrent '%1', reason: %2.)QBT_TR[CONTEXT=PushNotification]"
|
||||||
|
.replace("%1", payload.source)
|
||||||
|
.replace("%2", payload.reason);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
notificationTitle = "QBT_TR(Unsupported notification)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
notificationBody = "QBT_TR(An unsupported notification was received.)QBT_TR[CONTEXT=PushNotification]";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the service worker alive until the notification is created.
|
||||||
|
e.waitUntil(
|
||||||
|
self.registration.showNotification(notificationTitle, {
|
||||||
|
body: notificationBody,
|
||||||
|
icon: "images/qbittorrent-tray.svg"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", (e) => {
|
||||||
|
e.waitUntil(
|
||||||
|
self.clients.matchAll({
|
||||||
|
type: "window"
|
||||||
|
}).then((clientList) => {
|
||||||
|
for (const client of clientList) {
|
||||||
|
if ("focus" in client)
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
if (clients.openWindow)
|
||||||
|
return clients.openWindow("/");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
|
@ -119,6 +119,19 @@
|
||||||
<input type="checkbox" id="useVirtualList">
|
<input type="checkbox" id="useVirtualList">
|
||||||
<label for="useVirtualList">QBT_TR(Enable optimized table rendering (experimental))QBT_TR[CONTEXT=OptionsDialog]</label>
|
<label for="useVirtualList">QBT_TR(Enable optimized table rendering (experimental))QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||||
</div>
|
</div>
|
||||||
|
<fieldset class="settings">
|
||||||
|
<legend>QBT_TR(Push Notification)QBT_TR[CONTEXT=OptionsDialog]</legend>
|
||||||
|
<div class="formRow" style="margin-bottom: 3px;">
|
||||||
|
<span>QBT_TR(Status:)QBT_TR[CONTEXT=OptionsDialog]</span>
|
||||||
|
<b id="subscriptionStatus">QBT_TR(Unknown)QBT_TR[CONTEXT=OptionsDialog]</b>
|
||||||
|
<div style="display: flex;flex-direction: row; align-items: center;gap: 4px;">
|
||||||
|
<button disabled id="subscribePushNotificationButton" type="button" onclick="qBittorrent.Preferences.subscribePushNotification();">QBT_TR(Subscribe)QBT_TR[CONTEXT=OptionsDialog]</button>
|
||||||
|
<button id="sendTestNotificationButton" type="button" onclick="qBittorrent.Preferences.sendTestNotification();">QBT_TR(Send test
|
||||||
|
notification)QBT_TR[CONTEXT=OptionsDialog]</button>
|
||||||
|
<div id="pushNotificationSpinner" class="mochaSpinner" style="position: static;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1793,6 +1806,8 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
||||||
updateDynDnsSettings: updateDynDnsSettings,
|
updateDynDnsSettings: updateDynDnsSettings,
|
||||||
updateWebuiLocaleSelect: updateWebuiLocaleSelect,
|
updateWebuiLocaleSelect: updateWebuiLocaleSelect,
|
||||||
registerDynDns: registerDynDns,
|
registerDynDns: registerDynDns,
|
||||||
|
sendTestNotification: sendTestNotification,
|
||||||
|
subscribePushNotification: subscribePushNotification,
|
||||||
applyPreferences: applyPreferences
|
applyPreferences: applyPreferences
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2227,6 +2242,67 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
||||||
combobox.options[0].selected = true;
|
combobox.options[0].selected = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendTestNotification = () => {
|
||||||
|
document.getElementById("sendTestNotificationButton").disabled = true;
|
||||||
|
document.getElementById("subscribePushNotificationButton").disabled = true;
|
||||||
|
document.getElementById("pushNotificationSpinner").style.display = "inline-block";
|
||||||
|
window.qBittorrent.WebPush.sendTestNotification().catch((e) => {
|
||||||
|
alert(e.message);
|
||||||
|
}).finally(() => {
|
||||||
|
document.getElementById("sendTestNotificationButton").disabled = false;
|
||||||
|
document.getElementById("subscribePushNotificationButton").disabled = false;
|
||||||
|
document.getElementById("pushNotificationSpinner").style.display = "none";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribePushNotification = () => {
|
||||||
|
document.getElementById("pushNotificationSpinner").style.display = "inline-block";
|
||||||
|
document.getElementById("subscribePushNotificationButton").disabled = true;
|
||||||
|
window.qBittorrent.WebPush.isSubscribed().then((subscribed) => {
|
||||||
|
const promise = !subscribed
|
||||||
|
? window.qBittorrent.WebPush.subscribe()
|
||||||
|
: window.qBittorrent.WebPush.unsubscribe();
|
||||||
|
promise.catch((e) => {
|
||||||
|
alert(e.message);
|
||||||
|
}).finally(() => {
|
||||||
|
updatePushNotification();
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
alert(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePushNotification = () => {
|
||||||
|
const subscriptionStatus = document.getElementById("subscriptionStatus");
|
||||||
|
const subscribePushNotificationButton = document.getElementById("subscribePushNotificationButton");
|
||||||
|
const sendTestNotificationButton = document.getElementById("sendTestNotificationButton");
|
||||||
|
if (!window.qBittorrent.WebPush.isSupported()) {
|
||||||
|
subscriptionStatus.textContent = "QBT_TR(Unsupported)QBT_TR[CONTEXT=OptionsDialog]";
|
||||||
|
subscribePushNotificationButton.style.display = "none";
|
||||||
|
sendTestNotificationButton.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pushNotificationSpinner = document.getElementById("pushNotificationSpinner");
|
||||||
|
pushNotificationSpinner.style.display = "inline-block";
|
||||||
|
window.qBittorrent.WebPush.isSubscribed().then((subscribed) => {
|
||||||
|
subscriptionStatus.textContent = subscribed
|
||||||
|
? "QBT_TR(Subscribed)QBT_TR[CONTEXT=OptionsDialog]"
|
||||||
|
: "QBT_TR(Not Subscribed)QBT_TR[CONTEXT=OptionsDialog]";
|
||||||
|
subscribePushNotificationButton.textContent = subscribed
|
||||||
|
? "QBT_TR(Unsubscribe)QBT_TR[CONTEXT=OptionsDialog]"
|
||||||
|
: "QBT_TR(Subscribe)QBT_TR[CONTEXT=OptionsDialog]";
|
||||||
|
subscribePushNotificationButton.disabled = false;
|
||||||
|
sendTestNotificationButton.style.display = subscribed ? "inline-block" : "none";
|
||||||
|
pushNotificationSpinner.style.display = "none";
|
||||||
|
}).catch((e) => {
|
||||||
|
subscriptionStatus.textContent = "QBT_TR(Errored)QBT_TR[CONTEXT=OptionsDialog]";
|
||||||
|
subscribePushNotificationButton.disabled = true;
|
||||||
|
sendTestNotificationButton.style.display = "none";
|
||||||
|
pushNotificationSpinner.style.display = "none";
|
||||||
|
alert(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadPreferences = () => {
|
const loadPreferences = () => {
|
||||||
window.parent.qBittorrent.Cache.preferences.init({
|
window.parent.qBittorrent.Cache.preferences.init({
|
||||||
onSuccess: (pref) => {
|
onSuccess: (pref) => {
|
||||||
|
@ -3222,6 +3298,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
||||||
});
|
});
|
||||||
|
|
||||||
loadPreferences();
|
loadPreferences();
|
||||||
|
updatePushNotification();
|
||||||
|
|
||||||
window.qBittorrent.pathAutofill.attachPathAutofill();
|
window.qBittorrent.pathAutofill.attachPathAutofill();
|
||||||
};
|
};
|
||||||
|
|
|
@ -416,9 +416,11 @@
|
||||||
<file>private/scripts/search.js</file>
|
<file>private/scripts/search.js</file>
|
||||||
<file>private/scripts/statistics.js</file>
|
<file>private/scripts/statistics.js</file>
|
||||||
<file>private/scripts/torrent-content.js</file>
|
<file>private/scripts/torrent-content.js</file>
|
||||||
|
<file>private/scripts/webpush.js</file>
|
||||||
<file>private/setlocation.html</file>
|
<file>private/setlocation.html</file>
|
||||||
<file>private/shareratio.html</file>
|
<file>private/shareratio.html</file>
|
||||||
<file>private/speedlimit.html</file>
|
<file>private/speedlimit.html</file>
|
||||||
|
<file>private/sw-webui.js</file>
|
||||||
<file>private/views/about.html</file>
|
<file>private/views/about.html</file>
|
||||||
<file>private/views/aboutToolbar.html</file>
|
<file>private/views/aboutToolbar.html</file>
|
||||||
<file>private/views/confirmAutoTMM.html</file>
|
<file>private/views/confirmAutoTMM.html</file>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue