/* TODO: */ //const DEBUG_TONY_SAMA_FORK_MODE = true import { DOMPurify } from '../../../lib.js'; import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js'; import { deleteExtension, EMPTY_AUTHOR, extensionNames, getAuthorFromUrl, getContext, installExtension, renderExtensionTemplateAsync, isOfficialExtension } from '../../extensions.js'; import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; import { accountStorage } from '../../util/AccountStorage.js'; import { escapeHtml, flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; import { t, translate } from '../../i18n.js'; import { SlashCommandParser } from '/scripts/slash-commands/SlashCommandParser.js'; export { MODULE_NAME }; const MODULE_NAME = 'assets'; const DEBUG_PREFIX = ' '; let previewAudio = null; let ASSETS_JSON_URL = 'https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json'; // DBG //if (DEBUG_TONY_SAMA_FORK_MODE) // ASSETS_JSON_URL = "https://raw.githubusercontent.com/Tony-sama/SillyTavern-Content/main/index.json" let availableAssets = {}; let currentAssets = {}; //#############################// // Extension UI and Settings // //#############################// function filterAssets() { const searchValue = String($('#assets_search').val()).toLowerCase().trim(); const typeValue = String($('#assets_type_select').val()); if (typeValue === '') { $('#assets_menu .assets-list-div').show(); $('#assets_menu .assets-list-div h3').show(); } else { $('#assets_menu .assets-list-div h3').hide(); $('#assets_menu .assets-list-div').hide(); $(`#assets_menu .assets-list-div[data-type="${typeValue}"]`).show(); } if (searchValue === '') { $('#assets_menu .asset-block').show(); } else { $('#assets_menu .asset-block').hide(); $('#assets_menu .asset-block').filter(function () { return $(this).text().toLowerCase().includes(searchValue); }).show(); } } const KNOWN_TYPES = { 'extension': t`Extensions`, 'character': t`Characters`, 'ambient': t`Ambient sounds`, 'bgm': t`Background music`, 'blip': t`Blip sounds`, }; /** * Creates the download/delete button element for a single asset, with all interaction handlers attached. * @param {object} asset The asset data object, containing at least id, name, description and url fields * @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' * @param {number} index Index of the asset in the list of available assets of the same type, used to create a unique element ID * @returns {JQuery} The button element */ function createAssetButton(asset, assetType, index) { const elemId = `assets_install_${assetType}_${index}`; const element = $('
', { id: elemId, class: 'asset-download-button right_menu_button' }); const label = $(''); element.append(label); console.debug(DEBUG_PREFIX, 'Checking asset', asset.id, asset.url); const assetInstall = async function () { element.off('click'); label.removeClass('fa-download'); this.classList.add('asset-download-button-loading'); const result = await installAsset(asset.url, assetType, asset.id); if (!result) { this.classList.remove('asset-download-button-loading'); label.addClass('fa-download'); label.removeClass('fa-spinner'); label.removeClass('fa-spin'); element.on('click', assetInstall); return; } label.addClass('fa-check'); this.classList.remove('asset-download-button-loading'); element.on('click', assetDelete); element.on('mouseenter', function () { label.removeClass('fa-check'); label.addClass('fa-trash'); label.addClass('redOverlayGlow'); }).on('mouseleave', function () { label.addClass('fa-check'); label.removeClass('fa-trash'); label.removeClass('redOverlayGlow'); }); }; const assetDelete = async function () { if (assetType === 'character') { toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported'); await SlashCommandParser.commands.go.callback(null, asset.id); return; } element.off('click'); await deleteAsset(assetType, asset.id); label.removeClass('fa-check'); label.removeClass('redOverlayGlow'); label.removeClass('fa-trash'); label.addClass('fa-download'); element.off('mouseenter').off('mouseleave'); element.on('click', assetInstall); }; if (isAssetInstalled(assetType, asset.id)) { console.debug(DEBUG_PREFIX, 'installed, checked'); label.toggleClass('fa-download'); label.toggleClass('fa-check'); element.on('click', assetDelete); element.on('mouseenter', function () { label.removeClass('fa-check'); label.addClass('fa-trash'); label.addClass('redOverlayGlow'); }).on('mouseleave', function () { label.addClass('fa-check'); label.removeClass('fa-trash'); label.removeClass('redOverlayGlow'); }); } else { console.debug(DEBUG_PREFIX, 'not installed, unchecked'); element.prop('checked', false); element.on('click', assetInstall); } return element; } /** * Creates the full visual block element for a single asset. * @param {object} asset The asset data object, containing at least id, name, description and url fields * @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' * @param {JQuery} element The button element from createAssetButton * @returns {JQuery} The asset block element */ function createAssetBlock(asset, assetType, element) { console.debug(DEBUG_PREFIX, 'Created element for ', asset.id); const displayName = DOMPurify.sanitize(asset.name || asset.id); const description = DOMPurify.sanitize(asset.description || ''); const url = isValidUrl(asset.url) ? asset.url : ''; const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`; const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; const toolTag = assetType === 'extension' && asset.tool; const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR; const nameSpan = $('', { class: 'asset-name flex-container alignitemscenter' }) .append($('').text(displayName)) .append($('', { class: 'asset_preview', href: url, target: '_blank', title: title }) .append($('', { class: `fa-solid fa-sm ${previewIcon}` }))); if (toolTag) { const tagSpan = $('', { class: 'tag', title: t`Adds a function tool` }) .append($('', { class: 'fa-solid fa-sm fa-wrench' })) .append(document.createTextNode(` ${t`Tool`}`)); nameSpan.append(tagSpan); } nameSpan.append($('', { class: 'expander' })); if (author.name) { nameSpan.append($('', { href: author.url, target: '_blank', class: 'asset-author-info' }) .append($('', { class: 'fa-solid fa-at fa-xs' })) .append($('').text(author.name))); } const infoDiv = $('
', { class: 'flex-container flexFlowColumn flexNoGap wide100p overflowHidden' }) .append(nameSpan) .append($('', { class: 'asset-description' }).text(description)); const assetBlock = $('').append(element).append(infoDiv); assetBlock.find('.tag').on('click', function (e) { const a = document.createElement('a'); a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/'; a.target = '_blank'; a.click(); }); if (assetType === 'character') { if (asset.highlight) { nameSpan.append($('', { class: 'fa-solid fa-sm fa-trophy' })); } nameSpan.prepend($('
', { class: 'avatar' }).append($('', { src: asset.url, alt: displayName }))); } assetBlock.addClass('asset-block'); return assetBlock; } /** * Builds and appends the menu section for a single asset type. * @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' * @returns {Promise} */ async function buildAssetTypeSection(assetType) { const assetTypeMenu = $('
', { id: `assets_${assetType}_div`, class: 'assets-list-div' }); assetTypeMenu.attr('data-type', assetType); assetTypeMenu.append($('

').text(KNOWN_TYPES[assetType] || assetType)).hide(); if (assetType == 'extension') { assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation')); } for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a.name.localeCompare(b.name))) { const i = availableAssets[assetType].indexOf(asset); const element = createAssetButton(asset, assetType, i); const assetBlock = createAssetBlock(asset, assetType, element); if (assetType === 'extension') { const extensionBlockList = isOfficialExtension(asset.url) ? assetTypeMenu.find('.assets-list-extensions-official .assets-list-extensions') : assetTypeMenu.find('.assets-list-extensions-community .assets-list-extensions'); extensionBlockList.append(assetBlock); } else { assetTypeMenu.append(assetBlock); } } assetTypeMenu.appendTo('#assets_menu'); assetTypeMenu.on('click', 'a.asset_preview', previewAsset); } /** * Parses the fetched assets JSON and renders the full assets menu. * @param {object[]} json Array of asset objects, each containing at least id, name, description, url and type fields */ async function populateAssetsMenu(json) { availableAssets = {}; $('#assets_menu').empty(); console.debug(DEBUG_PREFIX, 'Received assets dictionary', json); for (const i of json) { if (availableAssets[i.type] === undefined) availableAssets[i.type] = []; availableAssets[i.type].push(i); } console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets); // First extensions, then everything else const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0); $('#assets_type_select').empty(); $('#assets_search').val(''); $('#assets_type_select').append($('