Redesign extension manager
| @@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] { | ||
| 65 | 65 | } |
| 66 | 66 | |
| 67 | 67 | .extensions_info .extension_enabled { |
| 68 | 68 | colorfont-weight: greenbold; |
| 69 | 69 | } |
| 70 | 70 | |
| 71 | 71 | .extensions_info .extension_disabled { |
| @@ -76,6 +76,42 @@ label[for="extensions_autoconnect"] { | ||
| 76 | 76 | color: gray; |
| 77 | 77 | } |
| 78 | 78 | |
| 79 | +.extensions_info .extension_modules { | |
| 80 | + font-size: 0.8em; | |
| 81 | + font-weight: normal; | |
| 82 | +} | |
| 83 | + | |
| 84 | +.extensions_info .extension_block { | |
| 85 | + display: flex; | |
| 86 | + flex-wrap: wrap; | |
| 87 | + padding: 10px; | |
| 88 | + margin-bottom: 5px; | |
| 89 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 90 | + border-radius: 10px; | |
| 91 | + align-items: center; | |
| 92 | + justify-content: space-between; | |
| 93 | + gap: 10px; | |
| 94 | +} | |
| 95 | + | |
| 96 | +.extensions_info .extension_name { | |
| 97 | + font-size: 1.05em; | |
| 98 | +} | |
| 99 | + | |
| 100 | +.extensions_info .extension_version { | |
| 101 | + opacity: 0.8; | |
| 102 | + font-size: 0.8em; | |
| 103 | + font-weight: normal; | |
| 104 | + margin-left: 5px; | |
| 105 | +} | |
| 106 | + | |
| 107 | +.extensions_info .extension_block a { | |
| 108 | + color: var(--SmartThemeBodyColor); | |
| 109 | +} | |
| 110 | + | |
| 111 | +.extensions_info .extension_name.update_available { | |
| 112 | + color: limegreen; | |
| 113 | +} | |
| 114 | + | |
| 79 | 115 | input.extension_missing[type="checkbox"] { |
| 80 | 116 | opacity: 0.5; |
| 81 | 117 | } |
| @@ -515,64 +515,64 @@ function addExtensionScript(name, manifest) { | ||
| 515 | 515 | * @param {boolean} isDisabled - Whether the extension is disabled or not. |
| 516 | 516 | * @param {boolean} isExternal - Whether the extension is external or not. |
| 517 | 517 | * @param {string} checkboxClass - The class for the checkbox HTML element. |
| 518 | 518 | * @return {Promise<string>} - The HTML string that represents the extension. |
| 519 | 519 | */ |
| 520 | 520 | async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { |
| 521 | 521 | const displayName = manifest.display_name; |
| 522 | 522 | let displayVersion = manifest.version ? ` v${manifest.version}` : ''; |
| 523 | - let isUpToDate = true; | |
| 523 | + const externalId = name.replace('third-party', ''); | |
| 524 | - let updateButton = ''; | |
| 525 | 524 | let originHtml = ''; |
| 526 | 525 | if (isExternal) { |
| 527 | - let data = await getExtensionVersion(name.replace('third-party', '')); | |
| 526 | + originHtml = '<a>'; | |
| 528 | - let branch = data.currentBranchName; | |
| 529 | - let commitHash = data.currentCommitHash; | |
| 530 | - let origin = data.remoteUrl; | |
| 531 | - isUpToDate = data.isUpToDate; | |
| 532 | - displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; | |
| 533 | - updateButton = isUpToDate ? | |
| 534 | - `<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Up to date"><i class="fa-solid fa-code-commit fa-fw"></i></button></span>` : | |
| 535 | - `<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button></span>`; | |
| 536 | - originHtml = `<a href="${origin}" target="_blank" rel="noopener noreferrer">`; | |
| 537 | 527 | } |
| 538 | 528 | |
| 539 | 529 | let toggleElement = isActive || isDisabled ? |
| 540 | 530 | `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` : |
| 541 | 531 | `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`; |
| 542 | 532 | |
| 543 | 533 | let deleteButton = isExternal ? `<span class="delete-button"><button class="btn_delete menu_button" data-name="${name.replace('third-party', '')externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button></span>` : ''; |
| 544 | - | |
| 534 | + let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : ''; | |
| 545 | - // if external, wrap the name in a link to the repo | |
| 535 | + let modulesInfo = ''; | |
| 546 | - | |
| 547 | - let extensionHtml = `<hr> | |
| 548 | - <h4> | |
| 549 | - ${updateButton} | |
| 550 | - ${deleteButton} | |
| 551 | - ${originHtml} | |
| 552 | - <span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}"> | |
| 553 | - ${DOMPurify.sanitize(displayName)}${displayVersion} | |
| 554 | - </span> | |
| 555 | - ${isExternal ? '</a>' : ''} | |
| 556 | - | |
| 557 | - <span style="float:right;">${toggleElement}</span> | |
| 558 | - </h4>`; | |
| 559 | 536 | |
| 560 | 537 | if (isActive && Array.isArray(manifest.optional)) { |
| 561 | 538 | const optional = new Set(manifest.optional); |
| 562 | 539 | modules.forEach(x => optional.delete(x)); |
| 563 | 540 | if (optional.size > 0) { |
| 564 | 541 | const optionalString = DOMPurify.sanitize([...optional].join(', ')); |
| 565 | 542 | extensionHtmlmodulesInfo += `<pdiv class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></pdiv>`; |
| 566 | 543 | } |
| 567 | 544 | } else if (!isDisabled) { // Neither active nor disabled |
| 568 | 545 | const requirements = new Set(manifest.requires); |
| 569 | 546 | modules.forEach(x => requirements.delete(x)); |
| 570 | 547 | if (requirements.size > 0) { |
| 571 | 548 | const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); |
| 572 | 549 | extensionHtmlmodulesInfo += `<pdiv class="extension_modules">Missing modules: <span class="failure">${requirementsString}</span></pdiv>`; |
| 573 | 550 | } |
| 574 | 551 | } |
| 575 | 552 | |
| 553 | + // if external, wrap the name in a link to the repo | |
| 554 | + | |
| 555 | + let extensionHtml = ` | |
| 556 | + <div class="extension_block" data-name="${externalId}"> | |
| 557 | + <div class="extension_toggle"> | |
| 558 | + ${toggleElement} | |
| 559 | + </div> | |
| 560 | + <div class="flexGrow"> | |
| 561 | + ${originHtml} | |
| 562 | + <span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}"> | |
| 563 | + <span class="extension_name">${DOMPurify.sanitize(displayName)}</span> | |
| 564 | + <span class="extension_version">${displayVersion}</span> | |
| 565 | + ${modulesInfo} | |
| 566 | + </span> | |
| 567 | + ${isExternal ? '</a>' : ''} | |
| 568 | + </div> | |
| 569 | + | |
| 570 | + <div class="extension_actions flex-container alignItemsCenter"> | |
| 571 | + ${updateButton} | |
| 572 | + ${deleteButton} | |
| 573 | + </div> | |
| 574 | + </div>`; | |
| 575 | + | |
| 576 | 576 | return extensionHtml; |
| 577 | 577 | } |
| 578 | 578 | |
| @@ -580,9 +580,9 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt | ||
| 580 | 580 | * Gets extension data and generates the corresponding HTML for displaying the extension. |
| 581 | 581 | * |
| 582 | 582 | * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest. |
| 583 | 583 | * @return {Promise<object>} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. |
| 584 | 584 | */ |
| 585 | 585 | async function getExtensionData(extension) { |
| 586 | 586 | const name = extension[0]; |
| 587 | 587 | const manifest = extension[1]; |
| 588 | 588 | const isActive = activeExtensions.has(name); |
| @@ -591,7 +591,7 @@ async function getExtensionData(extension) { | ||
| 591 | 591 | |
| 592 | 592 | const checkboxClass = isDisabled ? 'checkbox_disabled' : ''; |
| 593 | 593 | |
| 594 | 594 | const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass); |
| 595 | 595 | |
| 596 | 596 | return { isExternal, extensionHtml }; |
| 597 | 597 | } |
| @@ -616,40 +616,28 @@ function getModuleInformation() { | ||
| 616 | 616 | async function showExtensionsDetails() { |
| 617 | 617 | let popupPromise; |
| 618 | 618 | try { |
| 619 | 619 | const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>'); |
| 620 | 620 | const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3>').addClass('opacity50p</div>'); |
| 621 | 621 | const htmlLoading = $(`<h3div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5"> |
| 622 | 622 | <i class="fa-solid fa-spinner fa-spin"></i> |
| 623 | 623 | <span>Loading third-party extensions... Please wait...</span> |
| 624 | 624 | </h3div>`); |
| 625 | 625 | |
| 626 | - /** @type {Promise<any>[]} */ | |
| 626 | + htmlExternal.append(htmlLoading); | |
| 627 | - const promises = []; | |
| 628 | - const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); | |
| 629 | 627 | |
| 630 | - for (const extension of extensions) { | |
| 628 | + const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order).map(getExtensionData); | |
| 631 | - promises.push(getExtensionData(extension)); | |
| 632 | - } | |
| 633 | - | |
| 634 | - promises.forEach(promise => { | |
| 635 | - promise.then(value => { | |
| 636 | - const { isExternal, extensionHtml } = value; | |
| 637 | - const container = isExternal ? htmlExternal : htmlDefault; | |
| 638 | - container.append(extensionHtml); | |
| 639 | - }); | |
| 640 | - }); | |
| 641 | 629 | |
| 642 | 630 | Promise.allSettled(promises)extensions.then(forEach()value => { |
| 643 | - htmlLoading.remove(); | |
| 631 | + const { isExternal, extensionHtml } = value; | |
| 644 | - htmlExternal.removeClass('opacity50p'); | |
| 632 | + const container = isExternal ? htmlExternal : htmlDefault; | |
| 633 | + container.append(extensionHtml); | |
| 645 | 634 | }); |
| 646 | 635 | |
| 647 | 636 | const html = $('<div></div>') |
| 648 | 637 | .addClass('extensions_info') |
| 649 | - .append(getModuleInformation()) | |
| 650 | 638 | .append(htmlDefault) |
| 651 | 639 | .append(htmlLoadinghtmlExternal) |
| 652 | 640 | .append(htmlExternalgetModuleInformation()); |
| 653 | 641 | |
| 654 | 642 | /** @type {import('./popup.js').CustomPopupButton} */ |
| 655 | 643 | const updateAllButton = { |
| @@ -692,6 +680,7 @@ async function showExtensionsDetails() { | ||
| 692 | 680 | }, |
| 693 | 681 | }); |
| 694 | 682 | popupPromise = popup.show(); |
| 683 | + checkForUpdatesManual().finally(() => htmlLoading.remove()); | |
| 695 | 684 | } catch (error) { |
| 696 | 685 | toastr.error('Error loading extensions. See browser console for details.'); |
| 697 | 686 | console.error(error); |
| @@ -873,6 +862,52 @@ export function doDailyExtensionUpdatesCheck() { | ||
| 873 | 862 | }, 1); |
| 874 | 863 | } |
| 875 | 864 | |
| 865 | +async function checkForUpdatesManual() { | |
| 866 | + const promises = []; | |
| 867 | + for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { | |
| 868 | + const externalId = id.replace('third-party', ''); | |
| 869 | + const promise = new Promise(async (resolve, reject) => { | |
| 870 | + try { | |
| 871 | + const data = await getExtensionVersion(externalId); | |
| 872 | + const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); | |
| 873 | + if (extensionBlock) { | |
| 874 | + if (data.isUpToDate === false) { | |
| 875 | + const buttonElement = extensionBlock.querySelector('.btn_update'); | |
| 876 | + if (buttonElement) { | |
| 877 | + buttonElement.classList.remove('displayNone'); | |
| 878 | + } | |
| 879 | + const nameElement = extensionBlock.querySelector('.extension_name'); | |
| 880 | + if (nameElement) { | |
| 881 | + nameElement.classList.add('update_available'); | |
| 882 | + } | |
| 883 | + } | |
| 884 | + let branch = data.currentBranchName; | |
| 885 | + let commitHash = data.currentCommitHash; | |
| 886 | + let origin = data.remoteUrl; | |
| 887 | + | |
| 888 | + const originLink = extensionBlock.querySelector('a'); | |
| 889 | + if (originLink) { | |
| 890 | + originLink.href = origin; | |
| 891 | + originLink.target = '_blank'; | |
| 892 | + originLink.rel = 'noopener noreferrer'; | |
| 893 | + } | |
| 894 | + | |
| 895 | + const versionElement = extensionBlock.querySelector('.extension_version'); | |
| 896 | + if (versionElement) { | |
| 897 | + versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`; | |
| 898 | + } | |
| 899 | + } | |
| 900 | + resolve(); | |
| 901 | + } catch (error) { | |
| 902 | + console.error('Error checking for extension updates', error); | |
| 903 | + reject(); | |
| 904 | + } | |
| 905 | + }); | |
| 906 | + promises.push(promise); | |
| 907 | + } | |
| 908 | + return Promise.allSettled(promises); | |
| 909 | +} | |
| 910 | + | |
| 876 | 911 | /** |
| 877 | 912 | * Checks if there are updates available for 3rd-party extensions. |
| 878 | 913 | * @param {boolean} force Skip nag check |