animated bg thumb toggle (#4109) * Feat: Add toggle for animated background thumbnails This commit introduces a new toggle in the User Settings UI: "Load Animated Background Thumbnails". - When checked (default), animated background thumbnails (.gif, .mp4, animated .webp) load and display as usual in the background selection menu. - When unchecked, these animated thumbnails are replaced with a transparent PNG (`__transparent.png`) in the selection menu. This is intended to reduce UI and network lag for you if you have a large number of animated backgrounds. The actual functionality of setting any background (animated or static) remains unchanged; clicking a thumbnail will always apply the original background file. Changes include: - Added `loadAnimatedBackgroundThumbnails` (boolean, defaults to true) to `settings.json` under the `power_user` object. - Added the toggle HTML to `public/index.html`. - Modified `public/scripts/backgrounds.js` to conditionally replace animated thumbnails based on the new setting. - Modified `public/script.js` to handle the toggle's event listener and save its state. - Modified `public/scripts/power-user.js` to initialize the toggle's state upon loading settings. * Fix: Resolve issues with animated background thumbnail toggle This commit addresses two issues with the "Load Animated Background Thumbnails" feature: 1. Toggle state was not persisting correctly across page loads. 2. Animated media files were still being loaded even when the toggle was set to hide their thumbnails. Fixes: - Corrected the variable scope in `applyPowerUserSettings` within `public/scripts/power-user.js` to ensure the toggle's checked state is properly initialized from the global `power_user.loadAnimatedBackgroundThumbnails` setting. This resolves the persistence issue. - Refined the logic in `getBackgroundFromTemplate` within `public/scripts/backgrounds.js`. When the toggle is off and a background is an animated type (.gif, .mp4, .webp), the `background-image` CSS property for its thumbnail now directly uses the path to `__transparent.png`. This prevents the browser from potentially pre-fetching or the server from processing the original animated file via the `/thumbnail` endpoint for display purposes. - Added console logging in `getBackgroundFromTemplate` to help debug the toggle state and file being processed. The core functionality of applying backgrounds by clicking on thumbnails (whether transparent or regular) remains unchanged, as the `bgfile` attribute correctly stores the original media path. * Fix: Resolve page load error and animated thumbnail toggle issues This commit addresses a critical page loading error (`power_user is not defined` in `backgrounds.js`) and ensures the "Load Animated Background Thumbnails" toggle functions as intended. The page load error was caused by `getBackgrounds()` (and subsequently `getBackgroundFromTemplate()`) in `backgrounds.js` attempting to access `power_user.loadAnimatedBackgroundThumbnails` before the `power_user` object was fully initialized and its properties reliably accessible during the application's `firstLoadInit` sequence. Fixes: 1. **Explicit Setting Passing for Initial Load**: * Modified `getBackgrounds()` in `public/scripts/backgrounds.js` to accept `loadAnimatedThumbnailsSetting` as a parameter. * Modified `getBackgroundFromTemplate()` in `public/scripts/backgrounds.js` to accept and prioritize this passed parameter. It includes fallback logic to use the global `power_user.loadAnimatedBackgroundThumbnails` (primarily for event-driven updates like chat changes, which happen later) and then to a default of `true` if the setting is somehow unavailable. * Updated the call to `getBackgrounds()` in `public/script.js` during `firstLoadInit` to pass `power_user.loadAnimatedBackgroundThumbnails` explicitly after `power_user` settings are loaded. * Updated the toggle's `change` event listener in `public/script.js` to also pass the new setting state to `getBackgrounds()` for immediate refresh of system backgrounds. 2. **Toggle Persistence**: * Ensured the toggle's state in `public/scripts/power-user.js` (within `applyPowerUserSettings`) is correctly initialized using the global `power_user.loadAnimatedBackgroundThumbnails`. 3. **Media Loading for Thumbnails**: * The logic in `getBackgroundFromTemplate` now ensures that if animated thumbnails are toggled off, the `background-image` CSS directly uses `url("backgrounds/__transparent.png")`, avoiding calls to `getThumbnailUrl()` with original animated file paths, which might have caused unintended pre-fetching or server-side processing. This set of changes should resolve the page loading blockage and ensure the animated background thumbnail toggle works correctly for both display and network loading, while maintaining the functionality of applying original backgrounds. * Fix: Ensure correct initial state and first-toggle behavior for animated thumbnail toggle This commit addresses issues where the "Load Animated Background Thumbnails" toggle might not correctly display its default 'on' state (checked) on a clean install, and ensures the first toggle action takes immediate effect. Changes: - In `public/scripts/power-user.js`: - `loadPowerUserSettings`: Ensured that `power_user.loadAnimatedBackgroundThumbnails` is robustly defaulted to `true` if the key is missing or not a boolean (e.g., from an older settings file) *before* `applyPowerUserSettings` is called. - `applyPowerUserSettings`: Added console logging to trace the value of `power_user.loadAnimatedBackgroundThumbnails` just before setting the checkbox state. Ensured the checkbox `checked` property is set using `!!power_user.loadAnimatedBackgroundThumbnails` for explicit boolean conversion. The function already correctly referenced the global `power_user` object. - In `public/scripts/backgrounds.js`: - `getBackgroundFromTemplate`: This function's logic for determining the `loadSetting` (which decides if a transparent thumbnail is used) was previously refined to prioritize a passed parameter, then fall back to the global `power_user.loadAnimatedBackgroundThumbnails`, and then to `true`. Console logs were added to trace the resolved setting and file details. - In `public/script.js`: - `firstLoadInit`: The call to `getBackgrounds` already correctly passes the `power_user.loadAnimatedBackgroundThumbnails` after settings are loaded. - Event listener for `#loadAnimatedBackgroundThumbnails`: The logic to update the global `power_user` state and then call refresh functions (`getBackgrounds` and emitting `CHAT_CHANGED` for `getChatBackgroundsList`) appears to be in the correct synchronous order. These changes should ensure the toggle initializes to its correct visual state (checked) and that the first toggle from ON to OFF (and back) correctly and immediately updates both the visual display of thumbnails and the network loading behavior. * Fix: Address ESLint issues in background thumbnail toggle feature This commit resolves ESLint errors that were present in `public/scripts/backgrounds.js` and `public/scripts/power-user.js` related to the "Load Animated Background Thumbnails" feature. Changes: - I corrected indentation errors in `public/scripts/backgrounds.js` as reported by the linter. - I replaced direct use of `object.hasOwnProperty()` with `Object.prototype.hasOwnProperty.call(object, ...)` in both `backgrounds.js` and `power-user.js` to resolve the `no-prototype-builtins` ESLint error. - I ensured `power_user` is correctly imported and/or checked for definition (using `typeof power_user !== 'undefined'`) in `backgrounds.js` before accessing its properties, to satisfy linter concerns around `no-undef` for `power_user` in the context of the `getBackgroundFromTemplate` fallback logic. These changes primarily address code style and linting rules and are not intended to alter the functional behavior of the animated background thumbnail toggle, which was addressed in previous commits. * fix weird spacing for lint * Refactor animated background thumbnail settings and functionality * Remove comments * Add missing awaits --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -4742,6 +4742,10 @@ | ||
| 4742 | 4742 | <input id="never_resize_avatars" type="checkbox" /> |
| 4743 | 4743 | <small data-i18n="Never resize avatars">Never resize avatars</small> |
| 4744 | 4744 | </label> |
| 4745 | + <label for="background_thumbnails_animation" class="checkbox_label" title="Make animated backgrounds static in the selection menu. This will reduce UI and network lag if you have a large amount. This is only a change for the selection menu."> | |
| 4746 | + <input id="background_thumbnails_animation" type="checkbox" /> | |
| 4747 | + <small data-i18n="Animated background thumbnails">Animated background thumbnails</small> | |
| 4748 | + </label> | |
| 4745 | 4749 | <label class="checkbox_label" for="show_card_avatar_urls" title="Show actual file names on the disk, in the characters list display only." data-i18n="[title]Show actual file names on the disk, in the characters list display only"> |
| 4746 | 4750 | <input id="show_card_avatar_urls" type="checkbox" /> |
| 4747 | 4751 | <small data-i18n="Show avatar filenames">Show avatar filenames</small> |
| @@ -1,20 +1,36 @@ | ||
| 1 | 1 | import { Fuse, localforage } from '../lib.js'; |
| 2 | - | |
| 3 | 2 | import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js'; |
| 4 | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 5 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 6 | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 7 | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat } from './utils.js'; |
| 8 | 7 | import { t } from './i18n.js'; |
| 9 | 8 | import { Popup } from './popup.js'; |
| 10 | 9 | |
| 11 | 10 | const BG_METADATA_KEY = 'custom_background'; |
| 12 | 11 | const LIST_METADATA_KEY = 'chat_backgrounds'; |
| 13 | 12 | |
| 13 | +// A single transparent PNG pixel used as a placeholder for errored backgrounds | |
| 14 | +const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; | |
| 15 | +const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * Storage for frontend-generated background thumbnails. | |
| 19 | + * This is used to store thumbnails for backgrounds that cannot be generated on the server. | |
| 20 | + */ | |
| 21 | +const THUMBNAIL_STORAGE = localforage.createInstance({ name: 'SillyTavern_Thumbnails' }); | |
| 22 | + | |
| 23 | +/** | |
| 24 | + * Cache for thumbnail blob URLs. | |
| 25 | + * @type {Map<string, string>} | |
| 26 | + */ | |
| 27 | +const THUMBNAIL_BLOBS = new Map(); | |
| 28 | + | |
| 14 | 29 | export let background_settings = { |
| 15 | 30 | name: '__transparent.png', |
| 16 | 31 | url: generateUrlParameter('__transparent.png', false), |
| 17 | 32 | fitting: 'classic', |
| 33 | + animation: false, | |
| 18 | 34 | }; |
| 19 | 35 | |
| 20 | 36 | export function loadBackgroundSettings(settings) { |
| @@ -25,16 +41,20 @@ export function loadBackgroundSettings(settings) { | ||
| 25 | 41 | if (!backgroundSettings.fitting) { |
| 26 | 42 | backgroundSettings.fitting = 'classic'; |
| 27 | 43 | } |
| 44 | + if (!Object.hasOwn(backgroundSettings, 'animation')) { | |
| 45 | + backgroundSettings.animation = false; | |
| 46 | + } | |
| 28 | 47 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 29 | 48 | setFittingClass(backgroundSettings.fitting); |
| 30 | 49 | $('#background_fitting').val(backgroundSettings.fitting); |
| 50 | + $('#background_thumbnails_animation').prop('checked', background_settings.animation); | |
| 31 | 51 | } |
| 32 | 52 | |
| 33 | 53 | /** |
| 34 | 54 | * Sets the background for the current chat and adds it to the list of custom backgrounds. |
| 35 | 55 | * @param {{url: string, path:string}} backgroundInfo |
| 36 | 56 | */ |
| 37 | 57 | async function forceSetBackground(backgroundInfo) { |
| 38 | 58 | saveBackgroundMetadata(backgroundInfo.url); |
| 39 | 59 | setCustomBackground(); |
| 40 | 60 | |
| @@ -43,7 +63,7 @@ function forceSetBackground(backgroundInfo) { | ||
| 43 | 63 | list.push(bg); |
| 44 | 64 | chat_metadata[LIST_METADATA_KEY] = list; |
| 45 | 65 | saveMetadataDebounced(); |
| 46 | 66 | await getChatBackgroundsList(); |
| 47 | 67 | highlightNewBackground(bg); |
| 48 | 68 | highlightLockedBackground(); |
| 49 | 69 | } |
| @@ -56,11 +76,11 @@ async function onChatChanged() { | ||
| 56 | 76 | unsetCustomBackground(); |
| 57 | 77 | } |
| 58 | 78 | |
| 59 | 79 | await getChatBackgroundsList(); |
| 60 | 80 | highlightLockedBackground(); |
| 61 | 81 | } |
| 62 | 82 | |
| 63 | 83 | async function getChatBackgroundsList() { |
| 64 | 84 | const list = chat_metadata[LIST_METADATA_KEY]; |
| 65 | 85 | const listEmpty = !Array.isArray(list) || list.length === 0; |
| 66 | 86 | |
| @@ -72,7 +92,7 @@ function getChatBackgroundsList() { | ||
| 72 | 92 | } |
| 73 | 93 | |
| 74 | 94 | for (const bg of list) { |
| 75 | 95 | const template = await getBackgroundFromTemplate(bg, true); |
| 76 | 96 | $('#bg_custom_content').append(template); |
| 77 | 97 | } |
| 78 | 98 | } |
| @@ -224,7 +244,49 @@ async function onCopyToSystemBackgroundClick(e) { | ||
| 224 | 244 | const index = list.indexOf(bgNames.oldBg); |
| 225 | 245 | list.splice(index, 1); |
| 226 | 246 | saveMetadataDebounced(); |
| 227 | 247 | await getChatBackgroundsList(); |
| 248 | +} | |
| 249 | + | |
| 250 | +/** | |
| 251 | + * Gets a thumbnail for the background from storage or fetches it if not available. | |
| 252 | + * It caches the thumbnail in local storage and returns a blob URL for the thumbnail. | |
| 253 | + * If the thumbnail cannot be fetched, it returns a transparent PNG pixel as a fallback. | |
| 254 | + * @param {string} bg Background URL | |
| 255 | + * @returns {Promise<string>} Blob URL of the thumbnail | |
| 256 | + */ | |
| 257 | +async function getThumbnailFromStorage(bg) { | |
| 258 | + const cachedBlobUrl = THUMBNAIL_BLOBS.get(bg); | |
| 259 | + if (cachedBlobUrl) { | |
| 260 | + return cachedBlobUrl; | |
| 261 | + } | |
| 262 | + | |
| 263 | + const savedBlob = await THUMBNAIL_STORAGE.getItem(bg); | |
| 264 | + if (savedBlob) { | |
| 265 | + const savedBlobUrl = URL.createObjectURL(savedBlob); | |
| 266 | + THUMBNAIL_BLOBS.set(bg, savedBlobUrl); | |
| 267 | + return savedBlobUrl; | |
| 268 | + } | |
| 269 | + | |
| 270 | + try { | |
| 271 | + const response = await fetch(getBackgroundPath(bg), { cache: 'force-cache' }); | |
| 272 | + if (!response.ok) { | |
| 273 | + throw new Error('Fetch failed with status: ' + response.status); | |
| 274 | + } | |
| 275 | + const imageBlob = await response.blob(); | |
| 276 | + const imageBase64 = await getBase64Async(imageBlob); | |
| 277 | + const thumbnailBase64 = await createThumbnail(imageBase64); | |
| 278 | + const thumbnailBlob = await fetch(thumbnailBase64).then(res => res.blob()); | |
| 279 | + await THUMBNAIL_STORAGE.setItem(bg, thumbnailBlob); | |
| 280 | + const blobUrl = URL.createObjectURL(thumbnailBlob); | |
| 281 | + THUMBNAIL_BLOBS.set(bg, blobUrl); | |
| 282 | + return blobUrl; | |
| 283 | + } catch (error) { | |
| 284 | + console.error('Error fetching thumbnail, fallback image will be used:', error); | |
| 285 | + const fallbackBlob = PNG_PIXEL_BLOB; | |
| 286 | + const fallbackBlobUrl = URL.createObjectURL(fallbackBlob); | |
| 287 | + THUMBNAIL_BLOBS.set(bg, fallbackBlobUrl); | |
| 288 | + return fallbackBlobUrl; | |
| 289 | + } | |
| 228 | 290 | } |
| 229 | 291 | |
| 230 | 292 | /** |
| @@ -327,7 +389,7 @@ async function onDeleteBackgroundClick(e) { | ||
| 327 | 389 | } |
| 328 | 390 | |
| 329 | 391 | if (isCustom) { |
| 330 | 392 | await getChatBackgroundsList(); |
| 331 | 393 | saveMetadataDebounced(); |
| 332 | 394 | } |
| 333 | 395 | } |
| @@ -382,7 +444,7 @@ export async function getBackgrounds() { | ||
| 382 | 444 | //console.log(getData.length); |
| 383 | 445 | $('#bg_menu_content').children('div').remove(); |
| 384 | 446 | for (const bg of getData) { |
| 385 | 447 | const template = await getBackgroundFromTemplate(bg, false); |
| 386 | 448 | $('#bg_menu_content').append(template); |
| 387 | 449 | } |
| 388 | 450 | } |
| @@ -405,19 +467,27 @@ function generateUrlParameter(bg, isCustom) { | ||
| 405 | 467 | * Instantiates a background template |
| 406 | 468 | * @param {string} bg Path to background |
| 407 | 469 | * @param {boolean} isCustom Whether the background is custom |
| 408 | 470 | * @returns {Promise<JQuery<HTMLElement>>} Background template |
| 409 | 471 | */ |
| 410 | 472 | async function getBackgroundFromTemplate(bg, isCustom) { |
| 411 | 473 | const template = $('#background_template .bg_example').clone(); |
| 412 | - const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg); | |
| 413 | 474 | const url = generateUrlParameter(bg, isCustom); |
| 414 | 475 | const title = isCustom ? bg.split('/').pop() : bg; |
| 415 | 476 | const friendlyTitle = title.slice(0, title.lastIndexOf('.')); |
| 477 | + const fileExtension = bg.split('.').pop().toLowerCase(); | |
| 478 | + const isAnimated = ['mp4', 'webp'].includes(fileExtension); | |
| 479 | + const thumbnailUrl = isAnimated && !background_settings.animation | |
| 480 | + ? await getThumbnailFromStorage(bg) | |
| 481 | + : isCustom | |
| 482 | + ? bg | |
| 483 | + : getThumbnailUrl('bg', bg); | |
| 484 | + const thumbnailCssUrl = `url('${thumbnailUrl}')`; | |
| 485 | + | |
| 416 | 486 | template.attr('title', title); |
| 417 | 487 | template.attr('bgfile', bg); |
| 418 | 488 | template.attr('custom', String(isCustom)); |
| 419 | 489 | template.data('url', url); |
| 420 | 490 | template.css('background-image', `url('${thumbPath}')`thumbnailCssUrl); |
| 421 | 491 | template.find('.BGSampleTitle').text(friendlyTitle); |
| 422 | 492 | return template; |
| 423 | 493 | } |
| @@ -437,6 +507,12 @@ async function delBackground(bg) { | ||
| 437 | 507 | bg: bg, |
| 438 | 508 | }), |
| 439 | 509 | }); |
| 510 | + | |
| 511 | + await THUMBNAIL_STORAGE.removeItem(bg); | |
| 512 | + if (THUMBNAIL_BLOBS.has(bg)) { | |
| 513 | + URL.revokeObjectURL(THUMBNAIL_BLOBS.get(bg)); | |
| 514 | + THUMBNAIL_BLOBS.delete(bg); | |
| 515 | + } | |
| 440 | 516 | } |
| 441 | 517 | |
| 442 | 518 | async function onBackgroundUploadSelected() { |
| @@ -572,17 +648,20 @@ export function initBackgrounds() { | ||
| 572 | 648 | $('#auto_background').on('click', autoBackgroundCommand); |
| 573 | 649 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 574 | 650 | $('#bg-filter').on('input', onBackgroundFilterInput); |
| 575 | 651 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg', |
| 652 | + name: 'lockbg', | |
| 576 | 653 | callback: () => onLockBackgroundClick(new CustomEvent('click')), |
| 577 | 654 | aliases: ['bglock'], |
| 578 | 655 | helpString: 'Locks a background for the currently selected chat', |
| 579 | 656 | })); |
| 580 | 657 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg', |
| 658 | + name: 'unlockbg', | |
| 581 | 659 | callback: () => onUnlockBackgroundClick(new CustomEvent('click')), |
| 582 | 660 | aliases: ['bgunlock'], |
| 583 | 661 | helpString: 'Unlocks a background for the currently selected chat', |
| 584 | 662 | })); |
| 585 | 663 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg', |
| 664 | + name: 'autobg', | |
| 586 | 665 | callback: autoBackgroundCommand, |
| 587 | 666 | aliases: ['bgauto'], |
| 588 | 667 | helpString: 'Automatically changes the background based on the chat context using the AI request prompt', |
| @@ -593,4 +672,13 @@ export function initBackgrounds() { | ||
| 593 | 672 | setFittingClass(background_settings.fitting); |
| 594 | 673 | saveSettingsDebounced(); |
| 595 | 674 | }); |
| 675 | + | |
| 676 | + $('#background_thumbnails_animation').on('input', async function () { | |
| 677 | + background_settings.animation = !!$(this).prop('checked'); | |
| 678 | + saveSettingsDebounced(); | |
| 679 | + | |
| 680 | + // Refresh background thumbnails | |
| 681 | + await getBackgrounds(); | |
| 682 | + await onChatChanged(); | |
| 683 | + }); | |
| 596 | 684 | } |