Redesign extension manager

9960db0ae2ef5cae6d75486294b61ef52d85a700

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

2 files changed, +128 -57Showing whitespace changes
public/css/extensions-panel.css+37 -1
@@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] {
65}65}
6666
67.extensions_info .extension_enabled {67.extensions_info .extension_enabled {
68 color: green;68 font-weight: bold;
69}69}
7070
71.extensions_info .extension_disabled {71.extensions_info .extension_disabled {
@@ -76,6 +76,42 @@ label[for="extensions_autoconnect"] {
76 color: gray;76 color: gray;
77}77}
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
79input.extension_missing[type="checkbox"] {115input.extension_missing[type="checkbox"] {
80 opacity: 0.5;116 opacity: 0.5;
81}117}
public/scripts/extensions.js+91 -56
@@ -515,64 +515,64 @@ function addExtensionScript(name, manifest) {
515 * @param {boolean} isDisabled - Whether the extension is disabled or not.515 * @param {boolean} isDisabled - Whether the extension is disabled or not.
516 * @param {boolean} isExternal - Whether the extension is external or not.516 * @param {boolean} isExternal - Whether the extension is external or not.
517 * @param {string} checkboxClass - The class for the checkbox HTML element.517 * @param {string} checkboxClass - The class for the checkbox HTML element.
518 * @return {Promise<string>} - The HTML string that represents the extension.518 * @return {string} - The HTML string that represents the extension.
519 */519 */
520async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {520function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
521 const displayName = manifest.display_name;521 const displayName = manifest.display_name;
522 let displayVersion = manifest.version ? ` v${manifest.version}` : '';522 let displayVersion = manifest.version ? ` v${manifest.version}` : '';
523 let isUpToDate = true;523 const externalId = name.replace('third-party', '');
524 let updateButton = '';
525 let originHtml = '';524 let originHtml = '';
526 if (isExternal) {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 }
538528
539 let toggleElement = isActive || isDisabled ?529 let toggleElement = isActive || isDisabled ?
540 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :530 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
541 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;531 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
542532
543 let deleteButton = isExternal ? `<span class="delete-button"><button class="btn_delete menu_button" data-name="${name.replace('third-party', '')}" title="Delete"><i class="fa-solid fa-trash-can"></i></button></span>` : '';533 let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
544534 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 repo535 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
560 if (isActive && Array.isArray(manifest.optional)) {537 if (isActive && Array.isArray(manifest.optional)) {
561 const optional = new Set(manifest.optional);538 const optional = new Set(manifest.optional);
562 modules.forEach(x => optional.delete(x));539 modules.forEach(x => optional.delete(x));
563 if (optional.size > 0) {540 if (optional.size > 0) {
564 const optionalString = DOMPurify.sanitize([...optional].join(', '));541 const optionalString = DOMPurify.sanitize([...optional].join(', '));
565 extensionHtml += `<p>Optional modules: <span class="optional">${optionalString}</span></p>`;542 modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;
566 }543 }
567 } else if (!isDisabled) { // Neither active nor disabled544 } else if (!isDisabled) { // Neither active nor disabled
568 const requirements = new Set(manifest.requires);545 const requirements = new Set(manifest.requires);
569 modules.forEach(x => requirements.delete(x));546 modules.forEach(x => requirements.delete(x));
570 if (requirements.size > 0) {547 if (requirements.size > 0) {
571 const requirementsString = DOMPurify.sanitize([...requirements].join(', '));548 const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
572 extensionHtml += `<p>Missing modules: <span class="failure">${requirementsString}</span></p>`;549 modulesInfo = `<div class="extension_modules">Missing modules: <span class="failure">${requirementsString}</span></div>`;
573 }550 }
574 }551 }
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
576 return extensionHtml;576 return extensionHtml;
577}577}
578578
@@ -580,9 +580,9 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
580 * Gets extension data and generates the corresponding HTML for displaying the extension.580 * Gets extension data and generates the corresponding HTML for displaying the extension.
581 *581 *
582 * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.582 * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.
583 * @return {Promise<object>} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string.583 * @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string.
584 */584 */
585async function getExtensionData(extension) {585function getExtensionData(extension) {
586 const name = extension[0];586 const name = extension[0];
587 const manifest = extension[1];587 const manifest = extension[1];
588 const isActive = activeExtensions.has(name);588 const isActive = activeExtensions.has(name);
@@ -591,7 +591,7 @@ async function getExtensionData(extension) {
591591
592 const checkboxClass = isDisabled ? 'checkbox_disabled' : '';592 const checkboxClass = isDisabled ? 'checkbox_disabled' : '';
593593
594 const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass);594 const extensionHtml = generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass);
595595
596 return { isExternal, extensionHtml };596 return { isExternal, extensionHtml };
597}597}
@@ -616,40 +616,28 @@ function getModuleInformation() {
616async function showExtensionsDetails() {616async function showExtensionsDetails() {
617 let popupPromise;617 let popupPromise;
618 try {618 try {
619 const htmlDefault = $('<h3>Built-in Extensions:</h3>');619 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>');
620 const htmlExternal = $('<h3>Installed Extensions:</h3>').addClass('opacity50p');620 const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3></div>');
621 const htmlLoading = $(`<h3 class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">621 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
622 <i class="fa-solid fa-spinner fa-spin"></i>622 <i class="fa-solid fa-spinner fa-spin"></i>
623 <span>Loading third-party extensions... Please wait...</span>623 <span>Loading third-party extensions... Please wait...</span>
624 </h3>`);624 </div>`);
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 }
633629
634 promises.forEach(promise => {630 extensions.forEach(value => {
635 promise.then(value => {
636 const { isExternal, extensionHtml } = value;631 const { isExternal, extensionHtml } = value;
637 const container = isExternal ? htmlExternal : htmlDefault;632 const container = isExternal ? htmlExternal : htmlDefault;
638 container.append(extensionHtml);633 container.append(extensionHtml);
639 });634 });
640 });
641
642 Promise.allSettled(promises).then(() => {
643 htmlLoading.remove();
644 htmlExternal.removeClass('opacity50p');
645 });
646635
647 const html = $('<div></div>')636 const html = $('<div></div>')
648 .addClass('extensions_info')637 .addClass('extensions_info')
649 .append(getModuleInformation())
650 .append(htmlDefault)638 .append(htmlDefault)
651 .append(htmlLoading)639 .append(htmlExternal)
652 .append(htmlExternal);640 .append(getModuleInformation());
653641
654 /** @type {import('./popup.js').CustomPopupButton} */642 /** @type {import('./popup.js').CustomPopupButton} */
655 const updateAllButton = {643 const updateAllButton = {
@@ -692,6 +680,7 @@ async function showExtensionsDetails() {
692 },680 },
693 });681 });
694 popupPromise = popup.show();682 popupPromise = popup.show();
683 checkForUpdatesManual().finally(() => htmlLoading.remove());
695 } catch (error) {684 } catch (error) {
696 toastr.error('Error loading extensions. See browser console for details.');685 toastr.error('Error loading extensions. See browser console for details.');
697 console.error(error);686 console.error(error);
@@ -873,6 +862,52 @@ export function doDailyExtensionUpdatesCheck() {
873 }, 1);862 }, 1);
874}863}
875864
865async 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 * Checks if there are updates available for 3rd-party extensions.912 * Checks if there are updates available for 3rd-party extensions.
878 * @param {boolean} force Skip nag check913 * @param {boolean} force Skip nag check