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>

a1ed83a1baecc4a2c535fb39487ebd41e1afb8aa

L <123923688+Vibecoder9000@users.noreply.github.com>

Signed
2 files changed, +110 -18Ignore whitespace
public/index.html+4 -0
@@ -4742,6 +4742,10 @@
4742 <input id="never_resize_avatars" type="checkbox" />4742 <input id="never_resize_avatars" type="checkbox" />
4743 <small data-i18n="Never resize avatars">Never resize avatars</small>4743 <small data-i18n="Never resize avatars">Never resize avatars</small>
4744 </label>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 <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">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 <input id="show_card_avatar_urls" type="checkbox" />4750 <input id="show_card_avatar_urls" type="checkbox" />
4747 <small data-i18n="Show avatar filenames">Show avatar filenames</small>4751 <small data-i18n="Show avatar filenames">Show avatar filenames</small>
public/scripts/backgrounds.js+106 -18
@@ -1,20 +1,36 @@
1import { Fuse } from '../lib.js';1import { Fuse, localforage } from '../lib.js';
2
3import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';2import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
4import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
5import { SlashCommand } from './slash-commands/SlashCommand.js';4import { SlashCommand } from './slash-commands/SlashCommand.js';
6import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
7import { flashHighlight, stringFormat } from './utils.js';6import { createThumbnail, flashHighlight, getBase64Async, stringFormat } from './utils.js';
8import { t } from './i18n.js';7import { t } from './i18n.js';
9import { Popup } from './popup.js';8import { Popup } from './popup.js';
109
11const BG_METADATA_KEY = 'custom_background';10const BG_METADATA_KEY = 'custom_background';
12const LIST_METADATA_KEY = 'chat_backgrounds';11const LIST_METADATA_KEY = 'chat_backgrounds';
1312
13// A single transparent PNG pixel used as a placeholder for errored backgrounds
14const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
15const 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 */
21const THUMBNAIL_STORAGE = localforage.createInstance({ name: 'SillyTavern_Thumbnails' });
22
23/**
24 * Cache for thumbnail blob URLs.
25 * @type {Map<string, string>}
26 */
27const THUMBNAIL_BLOBS = new Map();
28
14export let background_settings = {29export let background_settings = {
15 name: '__transparent.png',30 name: '__transparent.png',
16 url: generateUrlParameter('__transparent.png', false),31 url: generateUrlParameter('__transparent.png', false),
17 fitting: 'classic',32 fitting: 'classic',
33 animation: false,
18};34};
1935
20export function loadBackgroundSettings(settings) {36export function loadBackgroundSettings(settings) {
@@ -25,16 +41,20 @@ export function loadBackgroundSettings(settings) {
25 if (!backgroundSettings.fitting) {41 if (!backgroundSettings.fitting) {
26 backgroundSettings.fitting = 'classic';42 backgroundSettings.fitting = 'classic';
27 }43 }
44 if (!Object.hasOwn(backgroundSettings, 'animation')) {
45 backgroundSettings.animation = false;
46 }
28 setBackground(backgroundSettings.name, backgroundSettings.url);47 setBackground(backgroundSettings.name, backgroundSettings.url);
29 setFittingClass(backgroundSettings.fitting);48 setFittingClass(backgroundSettings.fitting);
30 $('#background_fitting').val(backgroundSettings.fitting);49 $('#background_fitting').val(backgroundSettings.fitting);
50 $('#background_thumbnails_animation').prop('checked', background_settings.animation);
31}51}
3252
33/**53/**
34 * Sets the background for the current chat and adds it to the list of custom backgrounds.54 * Sets the background for the current chat and adds it to the list of custom backgrounds.
35 * @param {{url: string, path:string}} backgroundInfo55 * @param {{url: string, path:string}} backgroundInfo
36 */56 */
37function forceSetBackground(backgroundInfo) {57async function forceSetBackground(backgroundInfo) {
38 saveBackgroundMetadata(backgroundInfo.url);58 saveBackgroundMetadata(backgroundInfo.url);
39 setCustomBackground();59 setCustomBackground();
4060
@@ -43,7 +63,7 @@ function forceSetBackground(backgroundInfo) {
43 list.push(bg);63 list.push(bg);
44 chat_metadata[LIST_METADATA_KEY] = list;64 chat_metadata[LIST_METADATA_KEY] = list;
45 saveMetadataDebounced();65 saveMetadataDebounced();
46 getChatBackgroundsList();66 await getChatBackgroundsList();
47 highlightNewBackground(bg);67 highlightNewBackground(bg);
48 highlightLockedBackground();68 highlightLockedBackground();
49}69}
@@ -56,11 +76,11 @@ async function onChatChanged() {
56 unsetCustomBackground();76 unsetCustomBackground();
57 }77 }
5878
59 getChatBackgroundsList();79 await getChatBackgroundsList();
60 highlightLockedBackground();80 highlightLockedBackground();
61}81}
6282
63function getChatBackgroundsList() {83async function getChatBackgroundsList() {
64 const list = chat_metadata[LIST_METADATA_KEY];84 const list = chat_metadata[LIST_METADATA_KEY];
65 const listEmpty = !Array.isArray(list) || list.length === 0;85 const listEmpty = !Array.isArray(list) || list.length === 0;
6686
@@ -72,7 +92,7 @@ function getChatBackgroundsList() {
72 }92 }
7393
74 for (const bg of list) {94 for (const bg of list) {
75 const template = getBackgroundFromTemplate(bg, true);95 const template = await getBackgroundFromTemplate(bg, true);
76 $('#bg_custom_content').append(template);96 $('#bg_custom_content').append(template);
77 }97 }
78}98}
@@ -224,7 +244,49 @@ async function onCopyToSystemBackgroundClick(e) {
224 const index = list.indexOf(bgNames.oldBg);244 const index = list.indexOf(bgNames.oldBg);
225 list.splice(index, 1);245 list.splice(index, 1);
226 saveMetadataDebounced();246 saveMetadataDebounced();
227 getChatBackgroundsList();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 */
257async 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}
229291
230/**292/**
@@ -327,7 +389,7 @@ async function onDeleteBackgroundClick(e) {
327 }389 }
328390
329 if (isCustom) {391 if (isCustom) {
330 getChatBackgroundsList();392 await getChatBackgroundsList();
331 saveMetadataDebounced();393 saveMetadataDebounced();
332 }394 }
333 }395 }
@@ -382,7 +444,7 @@ export async function getBackgrounds() {
382 //console.log(getData.length);444 //console.log(getData.length);
383 $('#bg_menu_content').children('div').remove();445 $('#bg_menu_content').children('div').remove();
384 for (const bg of getData) {446 for (const bg of getData) {
385 const template = getBackgroundFromTemplate(bg, false);447 const template = await getBackgroundFromTemplate(bg, false);
386 $('#bg_menu_content').append(template);448 $('#bg_menu_content').append(template);
387 }449 }
388 }450 }
@@ -405,19 +467,27 @@ function generateUrlParameter(bg, isCustom) {
405 * Instantiates a background template467 * Instantiates a background template
406 * @param {string} bg Path to background468 * @param {string} bg Path to background
407 * @param {boolean} isCustom Whether the background is custom469 * @param {boolean} isCustom Whether the background is custom
408 * @returns {JQuery<HTMLElement>} Background template470 * @returns {Promise<JQuery<HTMLElement>>} Background template
409 */471 */
410function getBackgroundFromTemplate(bg, isCustom) {472async function getBackgroundFromTemplate(bg, isCustom) {
411 const template = $('#background_template .bg_example').clone();473 const template = $('#background_template .bg_example').clone();
412 const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg);
413 const url = generateUrlParameter(bg, isCustom);474 const url = generateUrlParameter(bg, isCustom);
414 const title = isCustom ? bg.split('/').pop() : bg;475 const title = isCustom ? bg.split('/').pop() : bg;
415 const friendlyTitle = title.slice(0, title.lastIndexOf('.'));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 template.attr('title', title);486 template.attr('title', title);
417 template.attr('bgfile', bg);487 template.attr('bgfile', bg);
418 template.attr('custom', String(isCustom));488 template.attr('custom', String(isCustom));
419 template.data('url', url);489 template.data('url', url);
420 template.css('background-image', `url('${thumbPath}')`);490 template.css('background-image', thumbnailCssUrl);
421 template.find('.BGSampleTitle').text(friendlyTitle);491 template.find('.BGSampleTitle').text(friendlyTitle);
422 return template;492 return template;
423}493}
@@ -437,6 +507,12 @@ async function delBackground(bg) {
437 bg: bg,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}
441517
442async function onBackgroundUploadSelected() {518async function onBackgroundUploadSelected() {
@@ -572,17 +648,20 @@ export function initBackgrounds() {
572 $('#auto_background').on('click', autoBackgroundCommand);648 $('#auto_background').on('click', autoBackgroundCommand);
573 $('#add_bg_button').on('change', onBackgroundUploadSelected);649 $('#add_bg_button').on('change', onBackgroundUploadSelected);
574 $('#bg-filter').on('input', onBackgroundFilterInput);650 $('#bg-filter').on('input', onBackgroundFilterInput);
575 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',651 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
652 name: 'lockbg',
576 callback: () => onLockBackgroundClick(new CustomEvent('click')),653 callback: () => onLockBackgroundClick(new CustomEvent('click')),
577 aliases: ['bglock'],654 aliases: ['bglock'],
578 helpString: 'Locks a background for the currently selected chat',655 helpString: 'Locks a background for the currently selected chat',
579 }));656 }));
580 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',657 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
658 name: 'unlockbg',
581 callback: () => onUnlockBackgroundClick(new CustomEvent('click')),659 callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
582 aliases: ['bgunlock'],660 aliases: ['bgunlock'],
583 helpString: 'Unlocks a background for the currently selected chat',661 helpString: 'Unlocks a background for the currently selected chat',
584 }));662 }));
585 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg',663 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
664 name: 'autobg',
586 callback: autoBackgroundCommand,665 callback: autoBackgroundCommand,
587 aliases: ['bgauto'],666 aliases: ['bgauto'],
588 helpString: 'Automatically changes the background based on the chat context using the AI request prompt',667 helpString: 'Automatically changes the background based on the chat context using the AI request prompt',
@@ -593,4 +672,13 @@ export function initBackgrounds() {
593 setFittingClass(background_settings.fitting);672 setFittingClass(background_settings.fitting);
594 saveSettingsDebounced();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}