Rearrange ext.panel. Add ext.update notifications. Improve performance on large number of extensions

This commit is contained in:
Cohee 2023-10-23 16:53:31 +03:00
parent e082138c18
commit f0b20b67de
3 changed files with 133 additions and 50 deletions

View File

@ -3282,14 +3282,14 @@
<div id="rm_extensions_block" class="drawer-content closedDrawer">
<div class="extensions_block flex-container">
<div class="alignitemscenter flex-container justifyCenter wide100p" style="justify-content: space-between;">
<h3 class="margin0" data-i18n="Extensions API:">Extensions API:
<a target="_blank" href="https://github.com/SillyTavern/SillyTavern-extras">
SillyTavern-extras
<h3 class="margin0" data-i18n="Extras API:">Extras API:
<a target="_blank" href="https://github.com/SillyTavern/SillyTavern-Extras">
SillyTavern-Extras
</a>
</h3>
<div class="flex-container">
<div id="extensions_status" data-i18n="Not connected...">Not connected...</div>
<label for="extensions_autoconnect">
<label for="extensions_autoconnect" class="checkbox_label flexNoGap">
<input id="extensions_autoconnect" type="checkbox">
<span data-i18n="Auto-connect">Auto-connect</span>
</label>
@ -3297,13 +3297,27 @@
</div>
<div class="alignitemsflexstart flex-container wide100p">
<input id="extensions_url" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]Extensions URL" placeholder="Extensions URL">
<input id="extensions_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]API key" placeholder="Extras API key">
<input id="extensions_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]API key" placeholder="Extras API key (optional)">
<div class="extensions_url_block">
<div id="extensions_connect" class="menu_button" data-i18n="Connect">Connect</div>
<div id="extensions_details" class="menu_button_icon menu_button">
Manage extensions
</div>
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
</div>
</div>
<hr class="wide100p">
<div class="alignitemscenter flex-container wide100p">
<h3 class="margin0 flex1" data-i18n="Extensions">
Extensions
</h3>
<label for="extensions_notify_updates" class="checkbox_label flexNoGap">
<input id="extensions_notify_updates" type="checkbox">
<span data-i18n="Notify on extension updates">Notify on extension updates</span>
</label>
<div id="extensions_details" class="menu_button_icon menu_button">
<i class="fa-solid fa-cubes"></i>
Manage extensions
</div>
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button menu_button_icon">
<i class="fa-solid fa-cloud-arrow-down"></i>
Install extension
</div>
</div>
<div id="extensions_settings" class="flex1 wide50p">

View File

@ -142,7 +142,7 @@ import {
onlyUnique,
} from "./scripts/utils.js";
import { extension_settings, getContext, installExtension, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
import { extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js";
import {
tag_map,
@ -8804,34 +8804,6 @@ jQuery(async function () {
}
});
/**
* Handles the click event for the third-party extension import button.
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
* If the extension is imported successfully, a success message is displayed.
* If the extension import fails, an error message is displayed and the error is logged to the console.
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
*
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to import</h3>
<br>
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension import cancelled');
return;
}
const url = input.trim();
await installExtension(url);
});
const $dropzone = $(document.body);
$dropzone.on('dragover', (event) => {

View File

@ -123,6 +123,7 @@ const extension_settings = {
apiUrl: defaultUrl,
apiKey: '',
autoConnect: false,
notifyUpdates: false,
disabledExtensions: [],
expressionOverrides: [],
memory: {},
@ -367,6 +368,15 @@ function addExtensionsButtonAndMenu() {
});
}
function notifyUpdatesInputHandler() {
extension_settings.notifyUpdates = !!$('#extensions_notify_updates').prop('checked');
saveSettingsDebounced();
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(true);
}
}
/* $(document).on('click', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
@ -582,16 +592,25 @@ async function showExtensionsDetails() {
let htmlExternal = '<h3>External Extensions:</h3>';
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];
for (const extension of extensions) {
const { isExternal, extensionHtml } = await getExtensionData(extension);
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
promises.push(getExtensionData(extension));
}
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach(promise => {
if (promise.status === 'fulfilled') {
const { isExternal, extensionHtml } = promise.value;
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
}
});
const html = `
${getModuleInformation()}
${htmlDefault}
@ -703,9 +722,9 @@ async function getExtensionVersion(extensionName) {
* @returns {Promise<void>}
*/
export async function installExtension(url) {
console.debug('Extension import started', url);
console.debug('Extension installation started', url);
toastr.info('Please wait...', 'Importing extension');
toastr.info('Please wait...', 'Installing extension');
const request = await fetch('/api/extensions/install', {
method: 'POST',
@ -714,14 +733,14 @@ export async function installExtension(url) {
});
if (!request.ok) {
toastr.info(request.statusText, 'Extension import failed');
console.error('Extension import failed', request.status, request.statusText);
toastr.info(request.statusText, 'Extension installation failed');
console.error('Extension installation failed', request.status, request.statusText);
return;
}
const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been imported successfully!`, 'Extension import successful');
console.debug(`Extension "${response.display_name}" has been imported successfully at ${response.extensionPath}`);
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false);
eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
}
@ -739,6 +758,7 @@ async function loadExtensionSettings(settings, versionChanged) {
$("#extensions_url").val(extension_settings.apiUrl);
$("#extensions_api_key").val(extension_settings.apiKey);
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);
$("#extensions_notify_updates").prop('checked', extension_settings.notifyUpdates);
// Activate offline extensions
eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
@ -754,6 +774,55 @@ async function loadExtensionSettings(settings, versionChanged) {
connectToApi(extension_settings.apiUrl);
}
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(false);
}
}
/**
* Checks if there are updates available for 3rd-party extensions.
* @param {boolean} force Skip nag check
* @returns {Promise<any>}
*/
async function checkForExtensionUpdates(force) {
if (!force) {
const STORAGE_NAG_KEY = 'extension_update_nag';
const currentDate = new Date().toDateString();
// Don't nag more than once a day
if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
return;
}
localStorage.setItem(STORAGE_NAG_KEY, currentDate);
}
const updatesAvailable = [];
const promises = [];
for (const [id, manifest] of Object.entries(manifests)) {
if (manifest.auto_update && id.startsWith('third-party')) {
const promise = new Promise(async (resolve, reject) => {
try {
const data = await getExtensionVersion(id.replace('third-party', ''));
if (data.isUpToDate === false) {
updatesAvailable.push(manifest.display_name);
}
resolve();
} catch (error) {
console.error('Error checking for extension updates', error);
reject();
}
});
promises.push(promise);
}
}
await Promise.allSettled(promises);
if (updatesAvailable.length > 0) {
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, 'Extension updates available');
}
}
async function autoUpdateExtensions() {
@ -785,8 +854,36 @@ jQuery(function () {
$("#extensions_connect").on('click', connectClickHandler);
$("#extensions_autoconnect").on('input', autoConnectInputHandler);
$("#extensions_details").on('click', showExtensionsDetails);
$("#extensions_notify_updates").on('input', notifyUpdatesInputHandler);
$(document).on('click', '.toggle_disable', onDisableExtensionClick);
$(document).on('click', '.toggle_enable', onEnableExtensionClick);
$(document).on('click', '.btn_update', onUpdateClick);
$(document).on('click', '.btn_delete', onDeleteClick);
/**
* Handles the click event for the third-party extension import button.
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
* If the extension is imported successfully, a success message is displayed.
* If the extension import fails, an error message is displayed and the error is logged to the console.
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
*
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to install</h3>
<br>
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension install cancelled');
return;
}
const url = input.trim();
await installExtension(url);
});
});