diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index da005bd70..6e0da7b3f 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] { } .extensions_info .extension_enabled { - color: green; + font-weight: bold; } .extensions_info .extension_disabled { @@ -76,6 +76,42 @@ label[for="extensions_autoconnect"] { color: gray; } +.extensions_info .extension_modules { + font-size: 0.8em; + font-weight: normal; +} + +.extensions_info .extension_block { + display: flex; + flex-wrap: wrap; + padding: 10px; + margin-bottom: 5px; + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 10px; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.extensions_info .extension_name { + font-size: 1.05em; +} + +.extensions_info .extension_version { + opacity: 0.8; + font-size: 0.8em; + font-weight: normal; + margin-left: 5px; +} + +.extensions_info .extension_block a { + color: var(--SmartThemeBodyColor); +} + +.extensions_info .extension_name.update_available { + color: limegreen; +} + input.extension_missing[type="checkbox"] { opacity: 0.5; } diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index e9a45f540..325f69451 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -515,64 +515,64 @@ function addExtensionScript(name, manifest) { * @param {boolean} isDisabled - Whether the extension is disabled or not. * @param {boolean} isExternal - Whether the extension is external or not. * @param {string} checkboxClass - The class for the checkbox HTML element. - * @return {Promise} - The HTML string that represents the extension. + * @return {string} - The HTML string that represents the extension. */ -async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { +function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { const displayName = manifest.display_name; let displayVersion = manifest.version ? ` v${manifest.version}` : ''; - let isUpToDate = true; - let updateButton = ''; + const externalId = name.replace('third-party', ''); let originHtml = ''; if (isExternal) { - let data = await getExtensionVersion(name.replace('third-party', '')); - let branch = data.currentBranchName; - let commitHash = data.currentCommitHash; - let origin = data.remoteUrl; - isUpToDate = data.isUpToDate; - displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; - updateButton = isUpToDate ? - `` : - ``; - originHtml = ``; + originHtml = ''; } let toggleElement = isActive || isDisabled ? `` : ``; - let deleteButton = isExternal ? `` : ''; - - // if external, wrap the name in a link to the repo - - let extensionHtml = `
-

- ${updateButton} - ${deleteButton} - ${originHtml} - - ${DOMPurify.sanitize(displayName)}${displayVersion} - - ${isExternal ? '' : ''} - - ${toggleElement} -

`; + let deleteButton = isExternal ? `` : ''; + let updateButton = isExternal ? `` : ''; + let modulesInfo = ''; if (isActive && Array.isArray(manifest.optional)) { const optional = new Set(manifest.optional); modules.forEach(x => optional.delete(x)); if (optional.size > 0) { const optionalString = DOMPurify.sanitize([...optional].join(', ')); - extensionHtml += `

Optional modules: ${optionalString}

`; + modulesInfo = `
Optional modules: ${optionalString}
`; } } else if (!isDisabled) { // Neither active nor disabled const requirements = new Set(manifest.requires); modules.forEach(x => requirements.delete(x)); if (requirements.size > 0) { const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); - extensionHtml += `

Missing modules: ${requirementsString}

`; + modulesInfo = `
Missing modules: ${requirementsString}
`; } } + // if external, wrap the name in a link to the repo + + let extensionHtml = ` +
+
+ ${toggleElement} +
+
+ ${originHtml} + + ${DOMPurify.sanitize(displayName)} + ${displayVersion} + ${modulesInfo} + + ${isExternal ? '' : ''} +
+ +
+ ${updateButton} + ${deleteButton} +
+
`; + return extensionHtml; } @@ -580,9 +580,9 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt * Gets extension data and generates the corresponding HTML for displaying the extension. * * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest. - * @return {Promise} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. + * @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. */ -async function getExtensionData(extension) { +function getExtensionData(extension) { const name = extension[0]; const manifest = extension[1]; const isActive = activeExtensions.has(name); @@ -591,7 +591,7 @@ async function getExtensionData(extension) { const checkboxClass = isDisabled ? 'checkbox_disabled' : ''; - const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass); + const extensionHtml = generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass); return { isExternal, extensionHtml }; } @@ -616,40 +616,28 @@ function getModuleInformation() { async function showExtensionsDetails() { let popupPromise; try { - const htmlDefault = $('

Built-in Extensions:

'); - const htmlExternal = $('

Installed Extensions:

').addClass('opacity50p'); - const htmlLoading = $(`

+ const htmlDefault = $('

Built-in Extensions:

'); + const htmlExternal = $('

Installed Extensions:

'); + const htmlLoading = $(`
Loading third-party extensions... Please wait... -

`); + `); - /** @type {Promise[]} */ - const promises = []; - const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); + htmlExternal.append(htmlLoading); - for (const extension of extensions) { - promises.push(getExtensionData(extension)); - } + const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order).map(getExtensionData); - promises.forEach(promise => { - promise.then(value => { - const { isExternal, extensionHtml } = value; - const container = isExternal ? htmlExternal : htmlDefault; - container.append(extensionHtml); - }); - }); - - Promise.allSettled(promises).then(() => { - htmlLoading.remove(); - htmlExternal.removeClass('opacity50p'); + extensions.forEach(value => { + const { isExternal, extensionHtml } = value; + const container = isExternal ? htmlExternal : htmlDefault; + container.append(extensionHtml); }); const html = $('
') .addClass('extensions_info') - .append(getModuleInformation()) .append(htmlDefault) - .append(htmlLoading) - .append(htmlExternal); + .append(htmlExternal) + .append(getModuleInformation()); /** @type {import('./popup.js').CustomPopupButton} */ const updateAllButton = { @@ -692,6 +680,7 @@ async function showExtensionsDetails() { }, }); popupPromise = popup.show(); + checkForUpdatesManual().finally(() => htmlLoading.remove()); } catch (error) { toastr.error('Error loading extensions. See browser console for details.'); console.error(error); @@ -873,6 +862,52 @@ export function doDailyExtensionUpdatesCheck() { }, 1); } +async function checkForUpdatesManual() { + const promises = []; + for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { + const externalId = id.replace('third-party', ''); + const promise = new Promise(async (resolve, reject) => { + try { + const data = await getExtensionVersion(externalId); + const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); + if (extensionBlock) { + if (data.isUpToDate === false) { + const buttonElement = extensionBlock.querySelector('.btn_update'); + if (buttonElement) { + buttonElement.classList.remove('displayNone'); + } + const nameElement = extensionBlock.querySelector('.extension_name'); + if (nameElement) { + nameElement.classList.add('update_available'); + } + } + let branch = data.currentBranchName; + let commitHash = data.currentCommitHash; + let origin = data.remoteUrl; + + const originLink = extensionBlock.querySelector('a'); + if (originLink) { + originLink.href = origin; + originLink.target = '_blank'; + originLink.rel = 'noopener noreferrer'; + } + + const versionElement = extensionBlock.querySelector('.extension_version'); + if (versionElement) { + versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`; + } + } + resolve(); + } catch (error) { + console.error('Error checking for extension updates', error); + reject(); + } + }); + promises.push(promise); + } + return Promise.allSettled(promises); +} + /** * Checks if there are updates available for 3rd-party extensions. * @param {boolean} force Skip nag check