mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-23 15:37:50 +01:00
Redesign extension manager
This commit is contained in:
parent
7ce2841588
commit
9960db0ae2
@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.extensions_info .extension_enabled {
|
.extensions_info .extension_enabled {
|
||||||
color: green;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extensions_info .extension_disabled {
|
.extensions_info .extension_disabled {
|
||||||
@ -76,6 +76,42 @@ label[for="extensions_autoconnect"] {
|
|||||||
color: gray;
|
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"] {
|
input.extension_missing[type="checkbox"] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
@ -515,64 +515,64 @@ function addExtensionScript(name, manifest) {
|
|||||||
* @param {boolean} isDisabled - Whether the extension is disabled or not.
|
* @param {boolean} isDisabled - Whether the extension is disabled or not.
|
||||||
* @param {boolean} isExternal - Whether the extension is external or not.
|
* @param {boolean} isExternal - Whether the extension is external or not.
|
||||||
* @param {string} checkboxClass - The class for the checkbox HTML element.
|
* @param {string} checkboxClass - The class for the checkbox HTML element.
|
||||||
* @return {Promise<string>} - 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;
|
const displayName = manifest.display_name;
|
||||||
let displayVersion = manifest.version ? ` v${manifest.version}` : '';
|
let displayVersion = manifest.version ? ` v${manifest.version}` : '';
|
||||||
let isUpToDate = true;
|
const externalId = name.replace('third-party', '');
|
||||||
let updateButton = '';
|
|
||||||
let originHtml = '';
|
let originHtml = '';
|
||||||
if (isExternal) {
|
if (isExternal) {
|
||||||
let data = await getExtensionVersion(name.replace('third-party', ''));
|
originHtml = '<a>';
|
||||||
let branch = data.currentBranchName;
|
|
||||||
let commitHash = data.currentCommitHash;
|
|
||||||
let origin = data.remoteUrl;
|
|
||||||
isUpToDate = data.isUpToDate;
|
|
||||||
displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`;
|
|
||||||
updateButton = isUpToDate ?
|
|
||||||
`<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>` :
|
|
||||||
`<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>`;
|
|
||||||
originHtml = `<a href="${origin}" target="_blank" rel="noopener noreferrer">`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let toggleElement = isActive || isDisabled ?
|
let toggleElement = isActive || isDisabled ?
|
||||||
`<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
|
`<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
|
||||||
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
|
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
|
||||||
|
|
||||||
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>` : '';
|
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>` : '';
|
||||||
|
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>` : '';
|
||||||
// if external, wrap the name in a link to the repo
|
let modulesInfo = '';
|
||||||
|
|
||||||
let extensionHtml = `<hr>
|
|
||||||
<h4>
|
|
||||||
${updateButton}
|
|
||||||
${deleteButton}
|
|
||||||
${originHtml}
|
|
||||||
<span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}">
|
|
||||||
${DOMPurify.sanitize(displayName)}${displayVersion}
|
|
||||||
</span>
|
|
||||||
${isExternal ? '</a>' : ''}
|
|
||||||
|
|
||||||
<span style="float:right;">${toggleElement}</span>
|
|
||||||
</h4>`;
|
|
||||||
|
|
||||||
if (isActive && Array.isArray(manifest.optional)) {
|
if (isActive && Array.isArray(manifest.optional)) {
|
||||||
const optional = new Set(manifest.optional);
|
const optional = new Set(manifest.optional);
|
||||||
modules.forEach(x => optional.delete(x));
|
modules.forEach(x => optional.delete(x));
|
||||||
if (optional.size > 0) {
|
if (optional.size > 0) {
|
||||||
const optionalString = DOMPurify.sanitize([...optional].join(', '));
|
const optionalString = DOMPurify.sanitize([...optional].join(', '));
|
||||||
extensionHtml += `<p>Optional modules: <span class="optional">${optionalString}</span></p>`;
|
modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;
|
||||||
}
|
}
|
||||||
} else if (!isDisabled) { // Neither active nor disabled
|
} else if (!isDisabled) { // Neither active nor disabled
|
||||||
const requirements = new Set(manifest.requires);
|
const requirements = new Set(manifest.requires);
|
||||||
modules.forEach(x => requirements.delete(x));
|
modules.forEach(x => requirements.delete(x));
|
||||||
if (requirements.size > 0) {
|
if (requirements.size > 0) {
|
||||||
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
|
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
|
||||||
extensionHtml += `<p>Missing modules: <span class="failure">${requirementsString}</span></p>`;
|
modulesInfo = `<div class="extension_modules">Missing modules: <span class="failure">${requirementsString}</span></div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if external, wrap the name in a link to the repo
|
||||||
|
|
||||||
|
let extensionHtml = `
|
||||||
|
<div class="extension_block" data-name="${externalId}">
|
||||||
|
<div class="extension_toggle">
|
||||||
|
${toggleElement}
|
||||||
|
</div>
|
||||||
|
<div class="flexGrow">
|
||||||
|
${originHtml}
|
||||||
|
<span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}">
|
||||||
|
<span class="extension_name">${DOMPurify.sanitize(displayName)}</span>
|
||||||
|
<span class="extension_version">${displayVersion}</span>
|
||||||
|
${modulesInfo}
|
||||||
|
</span>
|
||||||
|
${isExternal ? '</a>' : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="extension_actions flex-container alignItemsCenter">
|
||||||
|
${updateButton}
|
||||||
|
${deleteButton}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
return extensionHtml;
|
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.
|
* 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.
|
* @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.
|
||||||
* @return {Promise<object>} - 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 name = extension[0];
|
||||||
const manifest = extension[1];
|
const manifest = extension[1];
|
||||||
const isActive = activeExtensions.has(name);
|
const isActive = activeExtensions.has(name);
|
||||||
@ -591,7 +591,7 @@ async function getExtensionData(extension) {
|
|||||||
|
|
||||||
const checkboxClass = isDisabled ? 'checkbox_disabled' : '';
|
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 };
|
return { isExternal, extensionHtml };
|
||||||
}
|
}
|
||||||
@ -616,40 +616,28 @@ function getModuleInformation() {
|
|||||||
async function showExtensionsDetails() {
|
async function showExtensionsDetails() {
|
||||||
let popupPromise;
|
let popupPromise;
|
||||||
try {
|
try {
|
||||||
const htmlDefault = $('<h3>Built-in Extensions:</h3>');
|
const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>');
|
||||||
const htmlExternal = $('<h3>Installed Extensions:</h3>').addClass('opacity50p');
|
const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3></div>');
|
||||||
const htmlLoading = $(`<h3 class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
|
const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
|
||||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||||
<span>Loading third-party extensions... Please wait...</span>
|
<span>Loading third-party extensions... Please wait...</span>
|
||||||
</h3>`);
|
</div>`);
|
||||||
|
|
||||||
/** @type {Promise<any>[]} */
|
htmlExternal.append(htmlLoading);
|
||||||
const promises = [];
|
|
||||||
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
|
|
||||||
|
|
||||||
for (const extension of extensions) {
|
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order).map(getExtensionData);
|
||||||
promises.push(getExtensionData(extension));
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.forEach(promise => {
|
extensions.forEach(value => {
|
||||||
promise.then(value => {
|
|
||||||
const { isExternal, extensionHtml } = value;
|
const { isExternal, extensionHtml } = value;
|
||||||
const container = isExternal ? htmlExternal : htmlDefault;
|
const container = isExternal ? htmlExternal : htmlDefault;
|
||||||
container.append(extensionHtml);
|
container.append(extensionHtml);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
Promise.allSettled(promises).then(() => {
|
|
||||||
htmlLoading.remove();
|
|
||||||
htmlExternal.removeClass('opacity50p');
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = $('<div></div>')
|
const html = $('<div></div>')
|
||||||
.addClass('extensions_info')
|
.addClass('extensions_info')
|
||||||
.append(getModuleInformation())
|
|
||||||
.append(htmlDefault)
|
.append(htmlDefault)
|
||||||
.append(htmlLoading)
|
.append(htmlExternal)
|
||||||
.append(htmlExternal);
|
.append(getModuleInformation());
|
||||||
|
|
||||||
/** @type {import('./popup.js').CustomPopupButton} */
|
/** @type {import('./popup.js').CustomPopupButton} */
|
||||||
const updateAllButton = {
|
const updateAllButton = {
|
||||||
@ -692,6 +680,7 @@ async function showExtensionsDetails() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
popupPromise = popup.show();
|
popupPromise = popup.show();
|
||||||
|
checkForUpdatesManual().finally(() => htmlLoading.remove());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toastr.error('Error loading extensions. See browser console for details.');
|
toastr.error('Error loading extensions. See browser console for details.');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -873,6 +862,52 @@ export function doDailyExtensionUpdatesCheck() {
|
|||||||
}, 1);
|
}, 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.
|
* Checks if there are updates available for 3rd-party extensions.
|
||||||
* @param {boolean} force Skip nag check
|
* @param {boolean} force Skip nag check
|
||||||
|
Loading…
x
Reference in New Issue
Block a user