Feat/Allow to bulk toggle all third-party extensions from Manage Extensions (#5094) * Feat - Allow to bulk toggle all third-party extensions from popup manager * Fix - Prevent reloading the page if the final state is the same * Fix - Handle bulk toggle with no extensions installed * Update - Delete leftover debug logs * Fix - Simplify extension toggle logic and improve readability * Update - State that bulk toggle only affects external extensions * Feat - Allow to restore bulk toggled extensions * Update - Move bulk toggle to the third-party extensions header * Uncenter section headers --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

992fd8f01dadd14c1f9df824277018a02fcea3dd

Leandro Jofré <leandrotomasjofre@gmail.com>

Signed
2 files changed, +142 -4Ignore whitespace
public/css/extensions-panel.css+6 -2
@@ -39,7 +39,7 @@ label[for="extensions_autoconnect"] {
3939 text-align: left;
4040}
4141
4242.extensions_info h3:not(.margin0) {
4343 margin-bottom: 0.5em;
4444}
4545
@@ -112,6 +112,10 @@ label[for="extensions_autoconnect"] {
112112 color: limegreen;
113113}
114114
115+.extensions_info .third_party_toolbar {
116+ user-select: none;
117+}
118+
115119input.extension_missing[type="checkbox"] {
116120 opacity: 0.5;
117121}
@@ -157,4 +161,4 @@ input.extension_missing[type="checkbox"] {
157161 z-index: 1;
158162 margin-bottom: 10px;
159163 padding: 5px;
160164}
164 \ No newline at end of file
public/scripts/extensions.js+136 -2
@@ -300,6 +300,64 @@ function onEnableExtensionClick() {
300300}
301301
302302/**
303+ * Handles toggling all extensions on or off.
304+ * @param {Object[]} extensionsToToggle
305+ * @param {JQuery<HTMLElement>} toggleContainer
306+ * @returns {Object[]} Updated extensionsToToggle array
307+ */
308+function onToggleAllExtensions(extensionsToToggle, toggleContainer) {
309+ const extensionNames = Object.keys(manifests);
310+ const thirdPartyExtensions = extensionNames.filter(name => ['local', 'global'].includes(getExtensionType(name)));
311+
312+ const checkIfDisabled = (name) => {
313+ const toggle = extensionsToToggle.find(ext => ext.name === name);
314+ return toggle
315+ ? !toggle.enable
316+ : extension_settings.disabledExtensions.includes(name);
317+ };
318+
319+ if (thirdPartyExtensions.length === 0) return [];
320+
321+ let enable = true;
322+
323+ for (const name of thirdPartyExtensions) {
324+ const isEnabled = !checkIfDisabled(name);
325+
326+ if (isEnabled) {
327+ enable = false;
328+ break;
329+ }
330+ }
331+
332+ const toggleHandler = enable ? enableExtension : disableExtension;
333+
334+ for (const name of thirdPartyExtensions) {
335+ const isDisabled = checkIfDisabled(name);
336+ const doToggleExtension = enable ? isDisabled : !isDisabled;
337+
338+ if (doToggleExtension) {
339+ const toggle = extensionsToToggle.find(ext => ext.name === name);
340+
341+ if (toggle) {
342+ toggle.toggleHandler = toggleHandler;
343+ toggle.enable = enable;
344+ } else {
345+ extensionsToToggle.push({ name, toggleHandler, enable });
346+ }
347+
348+ toggleContainer
349+ .find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
350+ .prop('checked', enable)
351+ .toggleClass('toggle_enable', !enable)
352+ .toggleClass('toggle_disable', enable)
353+ .toggleClass('checkbox_disabled', !enable);
354+ }
355+ }
356+
357+ return extensionsToToggle;
358+}
359+
360+/**
303361 * Enables an extension by name.
304362 * @param {string} name Extension name
305363 * @param {boolean} [reload=true] If true, reload the page after enabling the extension
@@ -854,8 +912,15 @@ async function showExtensionsDetails() {
854912 await oldPopup.completeCancelled();
855913 }
856914 const htmlErrors = getExtensionLoadErrorsHtml();
857915 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');
858- const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');
916+
917+ const htmlExternal = $(`<div class="marginBot10">
918+ <div class="flex-container alignitemscenter spaceBetween flexnowrap marginBot10">
919+ <h3 class="margin0">${t`Installed Extensions:`}</h3>
920+ <div class="flex-container third_party_toolbar"></div>
921+ </div>
922+ </div>`);
923+
859924 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
860925 <i class="fa-solid fa-spinner fa-spin"></i>
861926 <span>` + t`Loading third-party extensions... Please wait...` + `</span>
@@ -867,6 +932,7 @@ async function showExtensionsDetails() {
867932 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
868933 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
869934 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
935+ let extensionsToToggle = [];
870936
871937 extensions.forEach(value => {
872938 const { isExternal, extensionHtml } = value;
@@ -901,6 +967,54 @@ async function showExtensionsDetails() {
901967 updateEnabledOnlyButton.textContent = t`Update enabled`;
902968 updateEnabledOnlyButton.addEventListener('click', () => updateAction(false));
903969
970+ const toggleAllExtensionsButton = document.createElement('div');
971+ toggleAllExtensionsButton.classList.add('menu_button', 'menu_button_icon');
972+ toggleAllExtensionsButton.title = t`Bulk toggle third-party extensions.`;
973+ toggleAllExtensionsButton.innerHTML = `
974+ <span>${t`Toggle extensions`}</span>
975+ <div class="fa-solid fa-circle-info opacity50p"></div>
976+ `;
977+
978+ const restoreBulkToggledExtensionsButton = document.createElement('div');
979+ restoreBulkToggledExtensionsButton.classList.add('menu_button', 'menu_button_icon', 'fa-solid', 'fa-arrow-right-rotate', 'displayNone');
980+ restoreBulkToggledExtensionsButton.title = t`Restore toggled extensions.\n\nIt does not restore extensions toggled individually.`;
981+
982+ toggleAllExtensionsButton.addEventListener('click', () => {
983+ extensionsToToggle = onToggleAllExtensions(extensionsToToggle, htmlExternal);
984+
985+ for (const extension of extensionsToToggle) {
986+ const { name } = extension;
987+
988+ htmlExternal
989+ .find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
990+ .off('click')
991+ .one('click', () => {
992+ extensionsToToggle = extensionsToToggle.filter(ext => ext.name !== name);
993+ });
994+ }
995+
996+ const restoreButtonHandler = extensionsToToggle.length > 0 ? 'remove' : 'add';
997+
998+ restoreBulkToggledExtensionsButton.classList[restoreButtonHandler]('displayNone');
999+ });
1000+
1001+ restoreBulkToggledExtensionsButton.addEventListener('click', () => {
1002+ for (const extension of extensionsToToggle) {
1003+ const { name } = extension;
1004+ const isDisabled = extension_settings.disabledExtensions.includes(name);
1005+
1006+ htmlExternal
1007+ .find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
1008+ .prop('checked', !isDisabled)
1009+ .toggleClass('toggle_enable', isDisabled)
1010+ .toggleClass('toggle_disable', !isDisabled)
1011+ .toggleClass('checkbox_disabled', isDisabled);
1012+ }
1013+
1014+ extensionsToToggle = [];
1015+ restoreBulkToggledExtensionsButton.classList.add('displayNone');
1016+ });
1017+
9041018 const flexExpander = document.createElement('div');
9051019 flexExpander.classList.add('expander');
9061020
@@ -914,6 +1028,7 @@ async function showExtensionsDetails() {
9141028 });
9151029
9161030 toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton);
1031+ htmlExternal.find('.third_party_toolbar').append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton);
9171032 html.prepend(toolbar);
9181033 }
9191034
@@ -929,6 +1044,24 @@ async function showExtensionsDetails() {
9291044 if (waitingForSave) {
9301045 return false;
9311046 }
1047+
1048+ for (const extension of extensionsToToggle) {
1049+ const { name, toggleHandler, enable } = extension;
1050+ const isDisabled = extension_settings.disabledExtensions.includes(name);
1051+
1052+ try {
1053+ if (isDisabled && !enable) continue;
1054+ if (!isDisabled && enable) continue;
1055+
1056+ requiresReload = true;
1057+
1058+ await toggleHandler(name, false);
1059+ } catch (error) {
1060+ console.error(`Could not toggle extension ${name}:`, error);
1061+ toastr.error(t`Could not toggle extension ${name}. See console for details.`);
1062+ }
1063+ }
1064+
9321065 if (stateChanged) {
9331066 waitingForSave = true;
9341067 const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`);
@@ -937,6 +1070,7 @@ async function showExtensionsDetails() {
9371070 waitingForSave = false;
9381071 requiresReload = true;
9391072 }
1073+
9401074 return true;
9411075 },
9421076 });