Redesign extension manager

9960db0ae2ef5cae6d75486294b61ef52d85a700

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +131 -60Ignore whitespace
public/css/extensions-panel.css+37 -1
@@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] {
6565}
6666
6767.extensions_info .extension_enabled {
6868 colorfont-weight: greenbold;
6969}
7070
7171.extensions_info .extension_disabled {
@@ -76,6 +76,42 @@ label[for="extensions_autoconnect"] {
7676 color: gray;
7777}
7878
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+
79115input.extension_missing[type="checkbox"] {
80116 opacity: 0.5;
81117}
public/scripts/extensions.js+94 -59
@@ -515,64 +515,64 @@ function addExtensionScript(name, manifest) {
515515 * @param {boolean} isDisabled - Whether the extension is disabled or not.
516516 * @param {boolean} isExternal - Whether the extension is external or not.
517517 * @param {string} checkboxClass - The class for the checkbox HTML element.
518518 * @return {Promise<string>} - The HTML string that represents the extension.
519519 */
520520async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
521521 const displayName = manifest.display_name;
522522 let displayVersion = manifest.version ? ` v${manifest.version}` : '';
523- let isUpToDate = true;
523+ const externalId = name.replace('third-party', '');
524- let updateButton = '';
525524 let originHtml = '';
526525 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">`;
537527 }
538528
539529 let toggleElement = isActive || isDisabled ?
540530 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
541531 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
542532
543533 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>`;
559536
560537 if (isActive && Array.isArray(manifest.optional)) {
561538 const optional = new Set(manifest.optional);
562539 modules.forEach(x => optional.delete(x));
563540 if (optional.size > 0) {
564541 const optionalString = DOMPurify.sanitize([...optional].join(', '));
565542 extensionHtmlmodulesInfo += `<pdiv class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></pdiv>`;
566543 }
567544 } else if (!isDisabled) { // Neither active nor disabled
568545 const requirements = new Set(manifest.requires);
569546 modules.forEach(x => requirements.delete(x));
570547 if (requirements.size > 0) {
571548 const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
572549 extensionHtmlmodulesInfo += `<pdiv class="extension_modules">Missing modules: <span class="failure">${requirementsString}</span></pdiv>`;
573550 }
574551 }
575552
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+
576576 return extensionHtml;
577577}
578578
@@ -580,9 +580,9 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
580580 * Gets extension data and generates the corresponding HTML for displaying the extension.
581581 *
582582 * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.
583583 * @return {Promise<object>} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string.
584584 */
585585async function getExtensionData(extension) {
586586 const name = extension[0];
587587 const manifest = extension[1];
588588 const isActive = activeExtensions.has(name);
@@ -591,7 +591,7 @@ async function getExtensionData(extension) {
591591
592592 const checkboxClass = isDisabled ? 'checkbox_disabled' : '';
593593
594594 const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass);
595595
596596 return { isExternal, extensionHtml };
597597}
@@ -616,40 +616,28 @@ function getModuleInformation() {
616616async function showExtensionsDetails() {
617617 let popupPromise;
618618 try {
619619 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>');
620620 const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3>').addClass('opacity50p</div>');
621621 const htmlLoading = $(`<h3div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
622622 <i class="fa-solid fa-spinner fa-spin"></i>
623623 <span>Loading third-party extensions... Please wait...</span>
624624 </h3div>`);
625625
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);
629627
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- });
641629
642630 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);
645634 });
646635
647636 const html = $('<div></div>')
648637 .addClass('extensions_info')
649- .append(getModuleInformation())
650638 .append(htmlDefault)
651639 .append(htmlLoadinghtmlExternal)
652640 .append(htmlExternalgetModuleInformation());
653641
654642 /** @type {import('./popup.js').CustomPopupButton} */
655643 const updateAllButton = {
@@ -692,6 +680,7 @@ async function showExtensionsDetails() {
692680 },
693681 });
694682 popupPromise = popup.show();
683+ checkForUpdatesManual().finally(() => htmlLoading.remove());
695684 } catch (error) {
696685 toastr.error('Error loading extensions. See browser console for details.');
697686 console.error(error);
@@ -873,6 +862,52 @@ export function doDailyExtensionUpdatesCheck() {
873862 }, 1);
874863}
875864
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+
876911/**
877912 * Checks if there are updates available for 3rd-party extensions.
878913 * @param {boolean} force Skip nag check