diff --git a/public/index.html b/public/index.html index ee2e883d1..6118a7f8c 100644 --- a/public/index.html +++ b/public/index.html @@ -3282,14 +3282,14 @@
-

Extensions API: - - SillyTavern-extras +

Extras API: + + SillyTavern-Extras

Not connected...
-
- +
- - +
+
+
+
+

+ Extensions +

+ + +
diff --git a/public/script.js b/public/script.js index ad0f5b96d..3b94f2650 100644 --- a/public/script.js +++ b/public/script.js @@ -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 = `

Enter the Git URL of the extension to import

-
-

Disclaimer: 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.

-
-

Example: https://github.com/author/extension-name

` - 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) => { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 20b72422a..0e519c125 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -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 = '

External Extensions:

'; 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} */ 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} + */ +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 = `

Enter the Git URL of the extension to install

+
+

Disclaimer: 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.

+
+

Example: https://github.com/author/extension-name

` + const input = await callPopup(html, 'input'); + + if (!input) { + console.debug('Extension install cancelled'); + return; + } + + const url = input.trim(); + await installExtension(url); + }); });