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, +141 -3Showing whitespace changes
public/css/extensions-panel.css+5 -1
@@ -39,7 +39,7 @@ label[for="extensions_autoconnect"] {
39 text-align: left;39 text-align: left;
40}40}
4141
42.extensions_info h3 {42.extensions_info h3:not(.margin0) {
43 margin-bottom: 0.5em;43 margin-bottom: 0.5em;
44}44}
4545
@@ -112,6 +112,10 @@ label[for="extensions_autoconnect"] {
112 color: limegreen;112 color: limegreen;
113}113}
114114
115.extensions_info .third_party_toolbar {
116 user-select: none;
117}
118
115input.extension_missing[type="checkbox"] {119input.extension_missing[type="checkbox"] {
116 opacity: 0.5;120 opacity: 0.5;
117}121}
public/scripts/extensions.js+136 -2
@@ -300,6 +300,64 @@ function onEnableExtensionClick() {
300}300}
301301
302/**302/**
303 * Handles toggling all extensions on or off.
304 * @param {Object[]} extensionsToToggle
305 * @param {JQuery<HTMLElement>} toggleContainer
306 * @returns {Object[]} Updated extensionsToToggle array
307 */
308function 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/**
303 * Enables an extension by name.361 * Enables an extension by name.
304 * @param {string} name Extension name362 * @param {string} name Extension name
305 * @param {boolean} [reload=true] If true, reload the page after enabling the extension363 * @param {boolean} [reload=true] If true, reload the page after enabling the extension
@@ -854,8 +912,15 @@ async function showExtensionsDetails() {
854 await oldPopup.completeCancelled();912 await oldPopup.completeCancelled();
855 }913 }
856 const htmlErrors = getExtensionLoadErrorsHtml();914 const htmlErrors = getExtensionLoadErrorsHtml();
857 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');915 const htmlDefault = $('<div class="marginBot10"><h3>' + 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
859 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">924 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
860 <i class="fa-solid fa-spinner fa-spin"></i>925 <i class="fa-solid fa-spinner fa-spin"></i>
861 <span>` + t`Loading third-party extensions... Please wait...` + `</span>926 <span>` + t`Loading third-party extensions... Please wait...` + `</span>
@@ -867,6 +932,7 @@ async function showExtensionsDetails() {
867 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';932 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
868 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;933 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
869 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);934 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
935 let extensionsToToggle = [];
870936
871 extensions.forEach(value => {937 extensions.forEach(value => {
872 const { isExternal, extensionHtml } = value;938 const { isExternal, extensionHtml } = value;
@@ -901,6 +967,54 @@ async function showExtensionsDetails() {
901 updateEnabledOnlyButton.textContent = t`Update enabled`;967 updateEnabledOnlyButton.textContent = t`Update enabled`;
902 updateEnabledOnlyButton.addEventListener('click', () => updateAction(false));968 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
904 const flexExpander = document.createElement('div');1018 const flexExpander = document.createElement('div');
905 flexExpander.classList.add('expander');1019 flexExpander.classList.add('expander');
9061020
@@ -914,6 +1028,7 @@ async function showExtensionsDetails() {
914 });1028 });
9151029
916 toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton);1030 toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton);
1031 htmlExternal.find('.third_party_toolbar').append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton);
917 html.prepend(toolbar);1032 html.prepend(toolbar);
918 }1033 }
9191034
@@ -929,6 +1044,24 @@ async function showExtensionsDetails() {
929 if (waitingForSave) {1044 if (waitingForSave) {
930 return false;1045 return false;
931 }1046 }
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
932 if (stateChanged) {1065 if (stateChanged) {
933 waitingForSave = true;1066 waitingForSave = true;
934 const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`);1067 const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`);
@@ -937,6 +1070,7 @@ async function showExtensionsDetails() {
937 waitingForSave = false;1070 waitingForSave = false;
938 requiresReload = true;1071 requiresReload = true;
939 }1072 }
1073
940 return true;1074 return true;
941 },1075 },
942 });1076 });