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 | text-align: left; | 39 | text-align: left; |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | .extensions_info h3 { | 42 | .extensions_info h3:not(.margin0) { |
| 43 | margin-bottom: 0.5em; | 43 | margin-bottom: 0.5em; |
| 44 | } | 44 | } |
| 45 | 45 | ||
| @@ -112,6 +112,10 @@ label[for="extensions_autoconnect"] { | |||
| 112 | color: limegreen; | 112 | color: limegreen; |
| 113 | } | 113 | } |
| 114 | 114 | ||
| 115 | .extensions_info .third_party_toolbar { | ||
| 116 | user-select: none; | ||
| 117 | } | ||
| 118 | |||
| 115 | input.extension_missing[type="checkbox"] { | 119 | input.extension_missing[type="checkbox"] { |
| 116 | opacity: 0.5; | 120 | opacity: 0.5; |
| 117 | } | 121 | } |
| @@ -157,4 +161,4 @@ input.extension_missing[type="checkbox"] { | |||
| 157 | z-index: 1; | 161 | z-index: 1; |
| 158 | margin-bottom: 10px; | 162 | margin-bottom: 10px; |
| 159 | padding: 5px; | 163 | padding: 5px; |
| 160 | } | 164 | } |
| \ No newline at end of file | 164 | \ No newline at end of file | |
| @@ -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 | * Enables an extension by name. | 361 | * Enables an extension by name. |
| 304 | * @param {string} name Extension name | 362 | * @param {string} name Extension name |
| 305 | * @param {boolean} [reload=true] If true, reload the page after enabling the extension | 363 | * @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 = []; | ||
| 870 | 936 | ||
| 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)); |
| 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 | const flexExpander = document.createElement('div'); | 1018 | const flexExpander = document.createElement('div'); |
| 905 | flexExpander.classList.add('expander'); | 1019 | flexExpander.classList.add('expander'); |
| 906 | 1020 | ||
| @@ -914,6 +1028,7 @@ async function showExtensionsDetails() { | |||
| 914 | }); | 1028 | }); |
| 915 | 1029 | ||
| 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 | } |
| 919 | 1034 | ||
| @@ -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 | }); |