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>
Signed| @@ -39,7 +39,7 @@ label[for="extensions_autoconnect"] { | ||
| 39 | 39 | text-align: left; |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | 42 | .extensions_info h3:not(.margin0) { |
| 43 | 43 | margin-bottom: 0.5em; |
| 44 | 44 | } |
| 45 | 45 | |
| @@ -112,6 +112,10 @@ label[for="extensions_autoconnect"] { | ||
| 112 | 112 | color: limegreen; |
| 113 | 113 | } |
| 114 | 114 | |
| 115 | +.extensions_info .third_party_toolbar { | |
| 116 | + user-select: none; | |
| 117 | +} | |
| 118 | + | |
| 115 | 119 | input.extension_missing[type="checkbox"] { |
| 116 | 120 | opacity: 0.5; |
| 117 | 121 | } |
| @@ -300,6 +300,64 @@ function onEnableExtensionClick() { | ||
| 300 | 300 | } |
| 301 | 301 | |
| 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 | + */ | |
| 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 | +/** | |
| 303 | 361 | * Enables an extension by name. |
| 304 | 362 | * @param {string} name Extension name |
| 305 | 363 | * @param {boolean} [reload=true] If true, reload the page after enabling the extension |
| @@ -854,8 +912,15 @@ async function showExtensionsDetails() { | ||
| 854 | 912 | await oldPopup.completeCancelled(); |
| 855 | 913 | } |
| 856 | 914 | const htmlErrors = getExtensionLoadErrorsHtml(); |
| 857 | 915 | 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 | + | |
| 859 | 924 | const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5"> |
| 860 | 925 | <i class="fa-solid fa-spinner fa-spin"></i> |
| 861 | 926 | <span>` + t`Loading third-party extensions... Please wait...` + `</span> |
| @@ -867,6 +932,7 @@ async function showExtensionsDetails() { | ||
| 867 | 932 | const sortByName = accountStorage.getItem(sortOrderKey) === 'true'; |
| 868 | 933 | const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder; |
| 869 | 934 | const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData); |
| 935 | + let extensionsToToggle = []; | |
| 870 | 936 | |
| 871 | 937 | extensions.forEach(value => { |
| 872 | 938 | const { isExternal, extensionHtml } = value; |
| @@ -901,6 +967,54 @@ async function showExtensionsDetails() { | ||
| 901 | 967 | updateEnabledOnlyButton.textContent = t`Update enabled`; |
| 902 | 968 | updateEnabledOnlyButton.addEventListener('click', () => updateAction(false)); |
| 903 | 969 | |
| 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 | 1018 | const flexExpander = document.createElement('div'); |
| 905 | 1019 | flexExpander.classList.add('expander'); |
| 906 | 1020 | |
| @@ -914,6 +1028,7 @@ async function showExtensionsDetails() { | ||
| 914 | 1028 | }); |
| 915 | 1029 | |
| 916 | 1030 | toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton); |
| 1031 | + htmlExternal.find('.third_party_toolbar').append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton); | |
| 917 | 1032 | html.prepend(toolbar); |
| 918 | 1033 | } |
| 919 | 1034 | |
| @@ -929,6 +1044,24 @@ async function showExtensionsDetails() { | ||
| 929 | 1044 | if (waitingForSave) { |
| 930 | 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 | 1065 | if (stateChanged) { |
| 933 | 1066 | waitingForSave = true; |
| 934 | 1067 | const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`); |
| @@ -937,6 +1070,7 @@ async function showExtensionsDetails() { | ||
| 937 | 1070 | waitingForSave = false; |
| 938 | 1071 | requiresReload = true; |
| 939 | 1072 | } |
| 1073 | + | |
| 940 | 1074 | return true; |
| 941 | 1075 | }, |
| 942 | 1076 | }); |