| 1 | /* |
| 2 | TODO: |
| 3 | */ |
| 4 | //const DEBUG_TONY_SAMA_FORK_MODE = true |
| 5 | |
| 6 | import { DOMPurify } from '../../../lib.js'; |
| 7 | import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js'; |
| 8 | import { deleteExtension, EMPTY_AUTHOR, extensionNames, getAuthorFromUrl, getContext, installExtension, renderExtensionTemplateAsync, isOfficialExtension } from '../../extensions.js'; |
| 9 | import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; |
| 10 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 11 | import { escapeHtml, flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; |
| 12 | import { t, translate } from '../../i18n.js'; |
| 13 | import { SlashCommandParser } from '/scripts/slash-commands/SlashCommandParser.js'; |
| 14 | export { MODULE_NAME }; |
| 15 | |
| 16 | const MODULE_NAME = 'assets'; |
| 17 | const DEBUG_PREFIX = '<Assets module> '; |
| 18 | let previewAudio = null; |
| 19 | let ASSETS_JSON_URL = 'https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json'; |
| 20 | |
| 21 | |
| 22 | // DBG |
| 23 | //if (DEBUG_TONY_SAMA_FORK_MODE) |
| 24 | // ASSETS_JSON_URL = "https://raw.githubusercontent.com/Tony-sama/SillyTavern-Content/main/index.json" |
| 25 | let availableAssets = {}; |
| 26 | let currentAssets = {}; |
| 27 | |
| 28 | //#############################// |
| 29 | // Extension UI and Settings // |
| 30 | //#############################// |
| 31 | |
| 32 | function filterAssets() { |
| 33 | const searchValue = String($('#assets_search').val()).toLowerCase().trim(); |
| 34 | const typeValue = String($('#assets_type_select').val()); |
| 35 | |
| 36 | if (typeValue === '') { |
| 37 | $('#assets_menu .assets-list-div').show(); |
| 38 | $('#assets_menu .assets-list-div h3').show(); |
| 39 | } else { |
| 40 | $('#assets_menu .assets-list-div h3').hide(); |
| 41 | $('#assets_menu .assets-list-div').hide(); |
| 42 | $(`#assets_menu .assets-list-div[data-type="${typeValue}"]`).show(); |
| 43 | } |
| 44 | |
| 45 | if (searchValue === '') { |
| 46 | $('#assets_menu .asset-block').show(); |
| 47 | } else { |
| 48 | $('#assets_menu .asset-block').hide(); |
| 49 | $('#assets_menu .asset-block').filter(function () { |
| 50 | return $(this).text().toLowerCase().includes(searchValue); |
| 51 | }).show(); |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | const KNOWN_TYPES = { |
| 56 | 'extension': t`Extensions`, |
| 57 | 'character': t`Characters`, |
| 58 | 'ambient': t`Ambient sounds`, |
| 59 | 'bgm': t`Background music`, |
| 60 | 'blip': t`Blip sounds`, |
| 61 | }; |
| 62 | |
| 63 | /** |
| 64 | * Creates the download/delete button element for a single asset, with all interaction handlers attached. |
| 65 | * @param {object} asset The asset data object, containing at least id, name, description and url fields |
| 66 | * @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' |
| 67 | * @param {number} index Index of the asset in the list of available assets of the same type, used to create a unique element ID |
| 68 | * @returns {JQuery} The button element |
| 69 | */ |
| 70 | function createAssetButton(asset, assetType, index) { |
| 71 | const elemId = `assets_install_${assetType}_${index}`; |
| 72 | const element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' }); |
| 73 | const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>'); |
| 74 | element.append(label); |
| 75 | |
| 76 | console.debug(DEBUG_PREFIX, 'Checking asset', asset.id, asset.url); |
| 77 | |
| 78 | const assetInstall = async function () { |
| 79 | element.off('click'); |
| 80 | label.removeClass('fa-download'); |
| 81 | this.classList.add('asset-download-button-loading'); |
| 82 | const result = await installAsset(asset.url, assetType, asset.id); |
| 83 | if (!result) { |
| 84 | this.classList.remove('asset-download-button-loading'); |
| 85 | label.addClass('fa-download'); |
| 86 | label.removeClass('fa-spinner'); |
| 87 | label.removeClass('fa-spin'); |
| 88 | element.on('click', assetInstall); |
| 89 | return; |
| 90 | } |
| 91 | label.addClass('fa-check'); |
| 92 | this.classList.remove('asset-download-button-loading'); |
| 93 | element.on('click', assetDelete); |
| 94 | element.on('mouseenter', function () { |
| 95 | label.removeClass('fa-check'); |
| 96 | label.addClass('fa-trash'); |
| 97 | label.addClass('redOverlayGlow'); |
| 98 | }).on('mouseleave', function () { |
| 99 | label.addClass('fa-check'); |
| 100 | label.removeClass('fa-trash'); |
| 101 | label.removeClass('redOverlayGlow'); |
| 102 | }); |
| 103 | }; |
| 104 | |
| 105 | const assetDelete = async function () { |
| 106 | if (assetType === 'character') { |
| 107 | toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported'); |
| 108 | await SlashCommandParser.commands.go.callback(null, asset.id); |
| 109 | return; |
| 110 | } |
| 111 | element.off('click'); |
| 112 | await deleteAsset(assetType, asset.id); |
| 113 | label.removeClass('fa-check'); |
| 114 | label.removeClass('redOverlayGlow'); |
| 115 | label.removeClass('fa-trash'); |
| 116 | label.addClass('fa-download'); |
| 117 | element.off('mouseenter').off('mouseleave'); |
| 118 | element.on('click', assetInstall); |
| 119 | }; |
| 120 | |
| 121 | if (isAssetInstalled(assetType, asset.id)) { |
| 122 | console.debug(DEBUG_PREFIX, 'installed, checked'); |
| 123 | label.toggleClass('fa-download'); |
| 124 | label.toggleClass('fa-check'); |
| 125 | element.on('click', assetDelete); |
| 126 | element.on('mouseenter', function () { |
| 127 | label.removeClass('fa-check'); |
| 128 | label.addClass('fa-trash'); |
| 129 | label.addClass('redOverlayGlow'); |
| 130 | }).on('mouseleave', function () { |
| 131 | label.addClass('fa-check'); |
| 132 | label.removeClass('fa-trash'); |
| 133 | label.removeClass('redOverlayGlow'); |
| 134 | }); |
| 135 | } else { |
| 136 | console.debug(DEBUG_PREFIX, 'not installed, unchecked'); |
| 137 | element.prop('checked', false); |
| 138 | element.on('click', assetInstall); |
| 139 | } |
| 140 | |
| 141 | return element; |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * Creates the full visual block element for a single asset. |
| 146 | * @param {object} asset The asset data object, containing at least id, name, description and url fields |
| 147 | * @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' |
| 148 | * @param {JQuery} element The button element from createAssetButton |
| 149 | * @returns {JQuery} The asset block element |
| 150 | */ |
| 151 | function createAssetBlock(asset, assetType, element) { |
| 152 | console.debug(DEBUG_PREFIX, 'Created element for ', asset.id); |
| 153 | |
| 154 | const displayName = DOMPurify.sanitize(asset.name || asset.id); |
| 155 | const description = DOMPurify.sanitize(asset.description || ''); |
| 156 | const url = isValidUrl(asset.url) ? asset.url : ''; |
| 157 | const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`; |
| 158 | const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; |
| 159 | const toolTag = assetType === 'extension' && asset.tool; |
| 160 | const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR; |
| 161 | |
| 162 | const nameSpan = $('<span>', { class: 'asset-name flex-container alignitemscenter' }) |
| 163 | .append($('<b>').text(displayName)) |
| 164 | .append($('<a>', { class: 'asset_preview', href: url, target: '_blank', title: title }) |
| 165 | .append($('<i>', { class: `fa-solid fa-sm ${previewIcon}` }))); |
| 166 | |
| 167 | if (toolTag) { |
| 168 | const tagSpan = $('<span>', { class: 'tag', title: t`Adds a function tool` }) |
| 169 | .append($('<i>', { class: 'fa-solid fa-sm fa-wrench' })) |
| 170 | .append(document.createTextNode(` ${t`Tool`}`)); |
| 171 | nameSpan.append(tagSpan); |
| 172 | } |
| 173 | |
| 174 | nameSpan.append($('<span>', { class: 'expander' })); |
| 175 | |
| 176 | if (author.name) { |
| 177 | nameSpan.append($('<a>', { href: author.url, target: '_blank', class: 'asset-author-info' }) |
| 178 | .append($('<i>', { class: 'fa-solid fa-at fa-xs' })) |
| 179 | .append($('<span>').text(author.name))); |
| 180 | } |
| 181 | |
| 182 | const infoDiv = $('<div>', { class: 'flex-container flexFlowColumn flexNoGap wide100p overflowHidden' }) |
| 183 | .append(nameSpan) |
| 184 | .append($('<small>', { class: 'asset-description' }).text(description)); |
| 185 | |
| 186 | const assetBlock = $('<i></i>').append(element).append(infoDiv); |
| 187 | |
| 188 | assetBlock.find('.tag').on('click', function (e) { |
| 189 | const a = document.createElement('a'); |
| 190 | a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/'; |
| 191 | a.target = '_blank'; |
| 192 | a.click(); |
| 193 | }); |
| 194 | |
| 195 | if (assetType === 'character') { |
| 196 | if (asset.highlight) { |
| 197 | nameSpan.append($('<i>', { class: 'fa-solid fa-sm fa-trophy' })); |
| 198 | } |
| 199 | nameSpan.prepend($('<div>', { class: 'avatar' }).append($('<img>', { src: asset.url, alt: displayName }))); |
| 200 | } |
| 201 | |
| 202 | assetBlock.addClass('asset-block'); |
| 203 | return assetBlock; |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Builds and appends the menu section for a single asset type. |
| 208 | * @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' |
| 209 | * @returns {Promise<void>} |
| 210 | */ |
| 211 | async function buildAssetTypeSection(assetType) { |
| 212 | const assetTypeMenu = $('<div />', { id: `assets_${assetType}_div`, class: 'assets-list-div' }); |
| 213 | assetTypeMenu.attr('data-type', assetType); |
| 214 | assetTypeMenu.append($('<h3>').text(KNOWN_TYPES[assetType] || assetType)).hide(); |
| 215 | |
| 216 | if (assetType == 'extension') { |
| 217 | assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation')); |
| 218 | } |
| 219 | |
| 220 | for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a.name.localeCompare(b.name))) { |
| 221 | const i = availableAssets[assetType].indexOf(asset); |
| 222 | const element = createAssetButton(asset, assetType, i); |
| 223 | const assetBlock = createAssetBlock(asset, assetType, element); |
| 224 | |
| 225 | if (assetType === 'extension') { |
| 226 | const extensionBlockList = isOfficialExtension(asset.url) |
| 227 | ? assetTypeMenu.find('.assets-list-extensions-official .assets-list-extensions') |
| 228 | : assetTypeMenu.find('.assets-list-extensions-community .assets-list-extensions'); |
| 229 | extensionBlockList.append(assetBlock); |
| 230 | } else { |
| 231 | assetTypeMenu.append(assetBlock); |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | assetTypeMenu.appendTo('#assets_menu'); |
| 236 | assetTypeMenu.on('click', 'a.asset_preview', previewAsset); |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Parses the fetched assets JSON and renders the full assets menu. |
| 241 | * @param {object[]} json Array of asset objects, each containing at least id, name, description, url and type fields |
| 242 | */ |
| 243 | async function populateAssetsMenu(json) { |
| 244 | availableAssets = {}; |
| 245 | $('#assets_menu').empty(); |
| 246 | |
| 247 | console.debug(DEBUG_PREFIX, 'Received assets dictionary', json); |
| 248 | |
| 249 | for (const i of json) { |
| 250 | if (availableAssets[i.type] === undefined) |
| 251 | availableAssets[i.type] = []; |
| 252 | availableAssets[i.type].push(i); |
| 253 | } |
| 254 | |
| 255 | console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets); |
| 256 | // First extensions, then everything else |
| 257 | const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0); |
| 258 | |
| 259 | $('#assets_type_select').empty(); |
| 260 | $('#assets_search').val(''); |
| 261 | $('#assets_type_select').append($('<option />', { value: '', text: t`All` })); |
| 262 | |
| 263 | for (const type of assetTypes) { |
| 264 | const text = translate(KNOWN_TYPES[type] || type); |
| 265 | const option = $('<option />', { value: type, text: text }); |
| 266 | $('#assets_type_select').append(option); |
| 267 | } |
| 268 | |
| 269 | if (assetTypes.includes('extension')) { |
| 270 | $('#assets_type_select').val('extension'); |
| 271 | } |
| 272 | |
| 273 | $('#assets_type_select').off('change').on('change', filterAssets); |
| 274 | $('#assets_search').off('input').on('input', filterAssets); |
| 275 | |
| 276 | for (const assetType of assetTypes) { |
| 277 | await buildAssetTypeSection(assetType); |
| 278 | } |
| 279 | |
| 280 | filterAssets(); |
| 281 | $('#assets_filters').show(); |
| 282 | $('#assets_menu').show(); |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * Downloads the assets list from the given URL and populates the menu. Shows error message if something goes wrong. |
| 287 | * @param {URL} url URL to fetch from |
| 288 | */ |
| 289 | async function downloadAssetsList(url) { |
| 290 | await updateCurrentAssets(); |
| 291 | try { |
| 292 | const response = await fetch(url, { cache: 'no-cache' }); |
| 293 | if (!response.ok) { |
| 294 | throw new Error('Cannot download the assets list.'); |
| 295 | } |
| 296 | const json = await response.json(); |
| 297 | if (!Array.isArray(json)) { |
| 298 | throw new Error('Assets list is not an array'); |
| 299 | } |
| 300 | await populateAssetsMenu(json); |
| 301 | } catch (error) { |
| 302 | // Info hint if the user maybe... likely accidentally was trying to install an extension and we wanna help guide them? uwu :3 |
| 303 | const installButton = $('#third_party_extension_button'); |
| 304 | flashHighlight(installButton, 10_000); |
| 305 | toastr.info('Click the flashing button at the top right corner of the menu.', 'Trying to install a custom extension?', { timeOut: 10_000 }); |
| 306 | |
| 307 | // Error logged after, to appear on top |
| 308 | console.error(error); |
| 309 | toastr.error('Problem with assets URL', 'Cannot get assets list'); |
| 310 | $('#assets-connect-button').addClass('fa-plug-circle-exclamation'); |
| 311 | $('#assets-connect-button').addClass('redOverlayGlow'); |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | /** |
| 316 | * Previews the asset by opening its URL. If it's an audio asset, it plays a preview sound. Otherwise, it opens the URL in a new tab. |
| 317 | * @param {JQuery.Event} e Click event |
| 318 | */ |
| 319 | function previewAsset(e) { |
| 320 | const href = $(this).attr('href'); |
| 321 | const audioExtensions = ['.mp3', '.ogg', '.wav']; |
| 322 | |
| 323 | if (audioExtensions.some(ext => href.endsWith(ext))) { |
| 324 | e.preventDefault(); |
| 325 | |
| 326 | if (previewAudio) { |
| 327 | previewAudio.pause(); |
| 328 | |
| 329 | if (previewAudio.src === href) { |
| 330 | previewAudio = null; |
| 331 | return; |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | previewAudio = new Audio(href); |
| 336 | previewAudio.play(); |
| 337 | return; |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * Checks if the asset is already installed. |
| 343 | * For extensions, it checks if the extension name is in the list of installed extensions. |
| 344 | * For characters, it checks if any character has the same avatar URL. |
| 345 | * For other asset types, it checks if any installed asset of the same type has a URL that includes the filename. |
| 346 | * @param {string} assetType Type of the asset, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' |
| 347 | * @param {string} filename Name or ID of the asset |
| 348 | * @returns {boolean} True if the asset is installed, false otherwise |
| 349 | */ |
| 350 | function isAssetInstalled(assetType, filename) { |
| 351 | let assetList = currentAssets[assetType]; |
| 352 | |
| 353 | if (assetType == 'extension') { |
| 354 | const thirdPartyMarker = 'third-party/'; |
| 355 | assetList = extensionNames.filter(x => x.startsWith(thirdPartyMarker)).map(x => x.replace(thirdPartyMarker, '')); |
| 356 | } |
| 357 | |
| 358 | if (assetType == 'character') { |
| 359 | assetList = getContext().characters.map(x => x.avatar); |
| 360 | } |
| 361 | |
| 362 | for (const i of assetList) { |
| 363 | //console.debug(DEBUG_PREFIX,i,filename) |
| 364 | if (i.includes(filename)) |
| 365 | return true; |
| 366 | } |
| 367 | |
| 368 | return false; |
| 369 | } |
| 370 | |
| 371 | /** |
| 372 | * Installs the asset by sending a request to the server to download it. If it's an extension, it uses the existing installExtension function. |
| 373 | * @param {string} url URL of the asset to download |
| 374 | * @param {string} assetType Type of the asset, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' |
| 375 | * @param {string} filename Name or ID of the asset |
| 376 | * @returns {Promise<boolean>} True if the asset was successfully installed, false otherwise |
| 377 | */ |
| 378 | async function installAsset(url, assetType, filename) { |
| 379 | console.debug(DEBUG_PREFIX, 'Downloading ', url); |
| 380 | const category = assetType; |
| 381 | try { |
| 382 | if (category === 'extension') { |
| 383 | console.debug(DEBUG_PREFIX, 'Installing extension ', url); |
| 384 | const result = await installExtension(url, false); |
| 385 | console.debug(DEBUG_PREFIX, 'Extension installed.'); |
| 386 | return result; |
| 387 | } |
| 388 | |
| 389 | const body = { url, category, filename }; |
| 390 | const result = await fetch('/api/assets/download', { |
| 391 | method: 'POST', |
| 392 | headers: getRequestHeaders(), |
| 393 | body: JSON.stringify(body), |
| 394 | cache: 'no-cache', |
| 395 | }); |
| 396 | if (result.ok) { |
| 397 | console.debug(DEBUG_PREFIX, 'Download success.'); |
| 398 | if (category === 'character') { |
| 399 | console.debug(DEBUG_PREFIX, 'Importing character ', filename); |
| 400 | const blob = await result.blob(); |
| 401 | const file = new File([blob], filename, { type: blob.type }); |
| 402 | const fileNameMap = new Map([[file, filename]]); |
| 403 | await processDroppedFiles([file], fileNameMap); |
| 404 | console.debug(DEBUG_PREFIX, 'Character downloaded.'); |
| 405 | } |
| 406 | return true; |
| 407 | } |
| 408 | return false; |
| 409 | } catch (err) { |
| 410 | console.log(err); |
| 411 | return false; |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | /** |
| 416 | * Deletes the asset by sending a request to the server to delete it. If it's an extension, it uses the existing deleteExtension function. |
| 417 | * @param {string} assetType Type of the asset, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip' |
| 418 | * @param {string} filename Name or ID of the asset |
| 419 | * @returns {Promise<boolean>} True if the asset was successfully deleted, false otherwise |
| 420 | */ |
| 421 | async function deleteAsset(assetType, filename) { |
| 422 | console.debug(DEBUG_PREFIX, 'Deleting ', assetType, filename); |
| 423 | const category = assetType; |
| 424 | try { |
| 425 | if (category === 'extension') { |
| 426 | console.debug(DEBUG_PREFIX, 'Deleting extension ', filename); |
| 427 | await deleteExtension(filename); |
| 428 | console.debug(DEBUG_PREFIX, 'Extension deleted.'); |
| 429 | return true; |
| 430 | } |
| 431 | |
| 432 | const body = { category, filename }; |
| 433 | const result = await fetch('/api/assets/delete', { |
| 434 | method: 'POST', |
| 435 | headers: getRequestHeaders(), |
| 436 | body: JSON.stringify(body), |
| 437 | cache: 'no-cache', |
| 438 | }); |
| 439 | if (result.ok) { |
| 440 | console.debug(DEBUG_PREFIX, 'Deletion success.'); |
| 441 | return true; |
| 442 | } |
| 443 | return false; |
| 444 | } catch (err) { |
| 445 | console.log(err); |
| 446 | return false; |
| 447 | } |
| 448 | } |
| 449 | |
| 450 | /** |
| 451 | * Opens the character browser popup, which shows all available characters and allows downloading them. |
| 452 | * @param {boolean} forceDefault If true, it uses the default ASSETS_JSON_URL instead of the one from the input field. |
| 453 | * @returns {Promise<void>} |
| 454 | */ |
| 455 | async function openCharacterBrowser(forceDefault) { |
| 456 | const url = forceDefault ? ASSETS_JSON_URL : String($('#assets-json-url-field').val()); |
| 457 | if (!isValidUrl(url)) { |
| 458 | toastr.error('Please enter a valid URL'); |
| 459 | return; |
| 460 | } |
| 461 | const fetchResult = await fetch(url, { cache: 'no-cache' }); |
| 462 | if (!fetchResult.ok) { |
| 463 | toastr.error('Cannot download the assets list.'); |
| 464 | return; |
| 465 | } |
| 466 | const json = await fetchResult.json(); |
| 467 | if (!Array.isArray(json)) { |
| 468 | toastr.error('Assets list is not an array'); |
| 469 | return; |
| 470 | } |
| 471 | const characters = json.filter(x => x && x.type === 'character'); |
| 472 | if (!characters.length) { |
| 473 | toastr.error('No characters found in the assets list', 'Character browser'); |
| 474 | return; |
| 475 | } |
| 476 | |
| 477 | const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'market', {})); |
| 478 | |
| 479 | for (const character of characters.sort((a, b) => a.name.localeCompare(b.name))) { |
| 480 | const listElement = template.find(character.highlight ? '.contestWinnersList' : '.featuredCharactersList'); |
| 481 | const characterElement = $(await renderExtensionTemplateAsync(MODULE_NAME, 'character', character)); |
| 482 | const downloadButton = characterElement.find('.characterAssetDownloadButton'); |
| 483 | const checkMark = characterElement.find('.characterAssetCheckMark'); |
| 484 | const isInstalled = isAssetInstalled('character', character.id); |
| 485 | |
| 486 | downloadButton.toggle(!isInstalled).on('click', async () => { |
| 487 | downloadButton.toggleClass('fa-download fa-spinner fa-spin'); |
| 488 | const result = await installAsset(character.url, 'character', character.id); |
| 489 | if (result) { |
| 490 | downloadButton.hide(); |
| 491 | checkMark.show(); |
| 492 | } else { |
| 493 | downloadButton.toggleClass('fa-download fa-spinner fa-spin'); |
| 494 | } |
| 495 | }); |
| 496 | |
| 497 | checkMark.toggle(isInstalled).on('click', async () => { |
| 498 | toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported'); |
| 499 | await SlashCommandParser.commands.go.callback(null, character.id); |
| 500 | }); |
| 501 | |
| 502 | listElement.append(characterElement); |
| 503 | } |
| 504 | |
| 505 | callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false }); |
| 506 | } |
| 507 | |
| 508 | //#############################// |
| 509 | // API Calls // |
| 510 | //#############################// |
| 511 | |
| 512 | async function updateCurrentAssets() { |
| 513 | console.debug(DEBUG_PREFIX, 'Checking installed assets...'); |
| 514 | try { |
| 515 | const result = await fetch('/api/assets/get', { |
| 516 | method: 'POST', |
| 517 | headers: getRequestHeaders({ omitContentType: true }), |
| 518 | }); |
| 519 | currentAssets = result.ok ? (await result.json()) : {}; |
| 520 | } catch (err) { |
| 521 | console.log(err); |
| 522 | } |
| 523 | console.debug(DEBUG_PREFIX, 'Current assets found:', currentAssets); |
| 524 | } |
| 525 | |
| 526 | |
| 527 | //#############################// |
| 528 | // Extension load // |
| 529 | //#############################// |
| 530 | |
| 531 | // This function is called when the extension is loaded |
| 532 | export async function init() { |
| 533 | // This is an example of loading HTML from a file |
| 534 | const windowTemplate = await renderExtensionTemplateAsync(MODULE_NAME, 'window', {}); |
| 535 | const windowHtml = $(windowTemplate); |
| 536 | |
| 537 | const assetsJsonUrl = windowHtml.find('#assets-json-url-field'); |
| 538 | assetsJsonUrl.val(ASSETS_JSON_URL); |
| 539 | |
| 540 | const charactersButton = windowHtml.find('#assets-characters-button'); |
| 541 | charactersButton.on('click', async function () { |
| 542 | openCharacterBrowser(false); |
| 543 | }); |
| 544 | |
| 545 | const installHintButton = windowHtml.find('.assets-install-hint-link'); |
| 546 | installHintButton.on('click', async function () { |
| 547 | const installButton = $('#third_party_extension_button'); |
| 548 | flashHighlight(installButton, 5000); |
| 549 | toastr.info(t`Click the flashing button to install extensions.`, t`How to install extensions?`); |
| 550 | }); |
| 551 | |
| 552 | const connectButton = windowHtml.find('#assets-connect-button'); |
| 553 | connectButton.on('click', async function () { |
| 554 | const urlString = String(assetsJsonUrl.val()).trim(); |
| 555 | if (!isValidUrl(urlString)) { |
| 556 | toastr.error('Please enter a valid URL'); |
| 557 | return; |
| 558 | } |
| 559 | const url = new URL(urlString); |
| 560 | const rememberKey = `Assets_SkipConfirm_${getStringHash(url.href)}`; |
| 561 | const skipConfirm = accountStorage.getItem(rememberKey) === 'true'; |
| 562 | |
| 563 | const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${escapeHtml(url.href)}</var>`, { |
| 564 | customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }], |
| 565 | onClose: popup => { |
| 566 | if (popup.result) { |
| 567 | const rememberValue = popup.inputResults.get('assets-remember'); |
| 568 | accountStorage.setItem(rememberKey, String(rememberValue)); |
| 569 | } |
| 570 | }, |
| 571 | }); |
| 572 | |
| 573 | if (confirmation) { |
| 574 | try { |
| 575 | console.debug(DEBUG_PREFIX, 'Confimation, loading assets...'); |
| 576 | downloadAssetsList(url); |
| 577 | connectButton.removeClass('fa-plug-circle-exclamation'); |
| 578 | connectButton.removeClass('redOverlayGlow'); |
| 579 | connectButton.addClass('fa-plug-circle-check'); |
| 580 | } catch (error) { |
| 581 | console.error('Error:', error); |
| 582 | toastr.error(`Cannot get assets list from ${url.href}`); |
| 583 | connectButton.removeClass('fa-plug-circle-check'); |
| 584 | connectButton.addClass('fa-plug-circle-exclamation'); |
| 585 | connectButton.removeClass('redOverlayGlow'); |
| 586 | } |
| 587 | } else { |
| 588 | console.debug(DEBUG_PREFIX, 'Connection refused by user'); |
| 589 | } |
| 590 | }); |
| 591 | |
| 592 | windowHtml.find('#assets_filters').hide(); |
| 593 | $('#assets_container').append(windowHtml); |
| 594 | |
| 595 | eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => { |
| 596 | openCharacterBrowser(forceDefault); |
| 597 | }); |
| 598 | } |