Layout Update (#4514) * init * mostly working messy version * css layout, mobile is fixed * revert changes * more of the above * cleanup * revert breaking fixes, cleanup dead code * dead code * delete fallback * spacing * css tweaks and removed getChatBackgroundsList * mobile sizing * revert unrelated changes * use name instead of context * debounce from constants.js * replace if/else * pass images directly * more of the above * buttons * buttons functionality * add default column counts * remove .mobile-only-menu-toggle when clicked (layering fix) * lint * universal column default of 3 * sacrifice firefox for chrome * Restore media query This reverts commit 295a5a81908e58c0bfa5e01fd90fcd62c05471b4. * Add disabled attribute styles for menu_button * use opacity 0, pointer events none instead of display:none * Format styles * Fix type errors * Add blur event handler to close mobile menu when focus is lost --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -88,6 +88,24 @@ body.reduced-motion #bg_custom { | |||
| 88 | gap: 5px; | 88 | gap: 5px; |
| 89 | } | 89 | } |
| 90 | 90 | ||
| 91 | /* Control buttons in header */ | ||
| 92 | .heading-container-with-controls { | ||
| 93 | position: relative; | ||
| 94 | } | ||
| 95 | |||
| 96 | .heading-container-with-controls .heading-text { | ||
| 97 | margin: 10px 0; | ||
| 98 | } | ||
| 99 | |||
| 100 | .heading-container-with-controls .heading-controls { | ||
| 101 | position: absolute; | ||
| 102 | right: 5px; | ||
| 103 | top: 50%; | ||
| 104 | transform: translateY(-50%); | ||
| 105 | display: flex; | ||
| 106 | gap: 5px; | ||
| 107 | } | ||
| 108 | |||
| 91 | #bg-scrollable-content { | 109 | #bg-scrollable-content { |
| 92 | flex-grow: 1; | 110 | flex-grow: 1; |
| 93 | overflow-y: auto; | 111 | overflow-y: auto; |
| @@ -95,46 +113,43 @@ body.reduced-motion #bg_custom { | |||
| 95 | padding: 0 5px 15px; | 113 | padding: 0 5px 15px; |
| 96 | } | 114 | } |
| 97 | 115 | ||
| 116 | #bg_menu_content, | ||
| 117 | #bg_custom_content { | ||
| 118 | display: grid; | ||
| 119 | gap: 5px; | ||
| 120 | width: 100%; | ||
| 121 | grid-template-columns: repeat(var(--bg-thumb-columns, 5), 1fr); | ||
| 122 | } | ||
| 123 | |||
| 98 | #bg-filter { | 124 | #bg-filter { |
| 99 | font-size: calc(var(--mainFontSize) * 0.95); | 125 | font-size: calc(var(--mainFontSize) * 0.95); |
| 100 | } | 126 | } |
| 101 | 127 | ||
| 102 | /* Thumbnail Menu & Buttons */ | 128 | /* Thumbnails */ |
| 129 | .bg_example:hover .BGSampleTitle { | ||
| 130 | opacity: 1; | ||
| 131 | } | ||
| 132 | |||
| 103 | .bg_example .mobile-only-menu-toggle { | 133 | .bg_example .mobile-only-menu-toggle { |
| 104 | display: none; | 134 | display: none; |
| 105 | } | 135 | } |
| 106 | 136 | ||
| 107 | .bg_example.flex-container { | 137 | .bg_example { |
| 108 | width: 30%; | ||
| 109 | max-width: 200px; | ||
| 110 | margin: 5px; | ||
| 111 | aspect-ratio: 16/9; | ||
| 112 | cursor: pointer; | 138 | cursor: pointer; |
| 113 | box-shadow: 0 0 7px var(--black50a); | 139 | box-shadow: 0 0 7px var(--black50a); |
| 114 | |||
| 115 | position: relative; | 140 | position: relative; |
| 116 | overflow: hidden; | 141 | overflow: hidden; |
| 117 | border-radius: 8px; | 142 | border-radius: 8px; |
| 118 | border: 0px solid transparent; | 143 | border: 0px solid transparent; |
| 119 | outline: 2px solid var(--SmartThemeBorderColor); | 144 | outline: 2px solid var(--SmartThemeBorderColor); |
| 120 | outline-offset: -1px; | 145 | outline-offset: -1px; |
| 121 | } | ||
| 122 | 146 | ||
| 123 | .bg_example.flex-container:focus-visible { | 147 | height: auto; |
| 124 | outline-offset: inherit; | 148 | aspect-ratio: 16 / 9; |
| 125 | } | 149 | } |
| 126 | 150 | ||
| 127 | .bg_example_img { | 151 | .bg_example:focus-visible { |
| 128 | position: absolute; | 152 | outline-offset: inherit; |
| 129 | top: -2px; | ||
| 130 | left: -2px; | ||
| 131 | right: -2px; | ||
| 132 | bottom: -2px; | ||
| 133 | |||
| 134 | background-image: inherit; | ||
| 135 | |||
| 136 | background-size: cover; | ||
| 137 | background-position: center; | ||
| 138 | } | 153 | } |
| 139 | 154 | ||
| 140 | .bg_example .jg-menu { | 155 | .bg_example .jg-menu { |
| @@ -143,9 +158,8 @@ body.reduced-motion #bg_custom { | |||
| 143 | top: 2px; | 158 | top: 2px; |
| 144 | right: 2px; | 159 | right: 2px; |
| 145 | background-color: rgba(0, 0, 0, 0.5); | 160 | background-color: rgba(0, 0, 0, 0.5); |
| 146 | border-radius: 8px; | 161 | border-radius: 5px; |
| 147 | gap: 3px; | 162 | padding: 3px 3px; |
| 148 | padding: 3px 5px; | ||
| 149 | z-index: 3; | 163 | z-index: 3; |
| 150 | backdrop-filter: blur(4px); | 164 | backdrop-filter: blur(4px); |
| 151 | border: 1px solid var(--SmartThemeBorderColor); | 165 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -168,14 +182,14 @@ body.reduced-motion #bg_custom { | |||
| 168 | 182 | ||
| 169 | .bg_example .jg-button { | 183 | .bg_example .jg-button { |
| 170 | display: flex; | 184 | display: flex; |
| 171 | width: 30px; | 185 | width: 24px; |
| 172 | height: 30px; | 186 | height: 24px; |
| 173 | align-items: center; | 187 | align-items: center; |
| 174 | justify-content: center; | 188 | justify-content: center; |
| 175 | color: white; | 189 | color: white; |
| 176 | padding: 5px; | 190 | padding: 5px; |
| 177 | font-size: 1.1em; | 191 | font-size: 1.1em; |
| 178 | border-radius: 6px; | 192 | border-radius: 5px; |
| 179 | transition: background-color var(--animation-duration) ease; | 193 | transition: background-color var(--animation-duration) ease; |
| 180 | } | 194 | } |
| 181 | 195 | ||
| @@ -195,6 +209,18 @@ body.reduced-motion #bg_custom { | |||
| 195 | display: flex; | 209 | display: flex; |
| 196 | } | 210 | } |
| 197 | 211 | ||
| 212 | .thumbnail-clipper { | ||
| 213 | position: absolute; | ||
| 214 | top: -2px; | ||
| 215 | left: -2px; | ||
| 216 | right: -2px; | ||
| 217 | bottom: -2px; | ||
| 218 | overflow: hidden; | ||
| 219 | border-radius: inherit; | ||
| 220 | background-size: cover; | ||
| 221 | background-position: center; | ||
| 222 | } | ||
| 223 | |||
| 198 | .bg_example:not([custom="true"]) .jg-copy, | 224 | .bg_example:not([custom="true"]) .jg-copy, |
| 199 | .bg_example[custom="true"] .jg-edit { | 225 | .bg_example[custom="true"] .jg-edit { |
| 200 | display: none; | 226 | display: none; |
| @@ -21,6 +21,14 @@ | |||
| 21 | display: none; | 21 | display: none; |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | #bg_menu_content, #bg_custom_content { | ||
| 25 | grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr); | ||
| 26 | } | ||
| 27 | |||
| 28 | .bg_list { | ||
| 29 | width: unset; | ||
| 30 | } | ||
| 31 | |||
| 24 | .bg_button { | 32 | .bg_button { |
| 25 | font-size: 15px; | 33 | font-size: 15px; |
| 26 | } | 34 | } |
| @@ -40,6 +48,11 @@ | |||
| 40 | z-index: 4; | 48 | z-index: 4; |
| 41 | } | 49 | } |
| 42 | 50 | ||
| 51 | .bg_example.mobile-menu-open .mobile-only-menu-toggle { | ||
| 52 | opacity: 0; | ||
| 53 | pointer-events: none; | ||
| 54 | } | ||
| 55 | |||
| 43 | .bg_example .mobile-only-menu-toggle { | 56 | .bg_example .mobile-only-menu-toggle { |
| 44 | display: flex; | 57 | display: flex; |
| 45 | align-items: center; | 58 | align-items: center; |
| @@ -57,6 +70,11 @@ | |||
| 57 | backdrop-filter: blur(2px); | 70 | backdrop-filter: blur(2px); |
| 58 | } | 71 | } |
| 59 | 72 | ||
| 73 | .bg_example .jg-button { | ||
| 74 | width: 30px; | ||
| 75 | height: 30px; | ||
| 76 | } | ||
| 77 | |||
| 60 | #bg-header-controls { | 78 | #bg-header-controls { |
| 61 | flex-wrap: wrap; | 79 | flex-wrap: wrap; |
| 62 | row-gap: 10px; | 80 | row-gap: 10px; |
| @@ -429,10 +447,6 @@ | |||
| 429 | .horde_multiple_hint { | 447 | .horde_multiple_hint { |
| 430 | display: none; | 448 | display: none; |
| 431 | } | 449 | } |
| 432 | |||
| 433 | .bg_list { | ||
| 434 | width: unset; | ||
| 435 | } | ||
| 436 | } | 450 | } |
| 437 | 451 | ||
| 438 | /*landscape mode phones and ipads*/ | 452 | /*landscape mode phones and ipads*/ |
| @@ -5345,9 +5345,19 @@ | |||
| 5345 | </div> | 5345 | </div> |
| 5346 | </div> | 5346 | </div> |
| 5347 | <div id="bg-scrollable-content"> | 5347 | <div id="bg-scrollable-content"> |
| 5348 | <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter"> | 5348 | <div class="heading-container-with-controls"> |
| 5349 | System Backgrounds | 5349 | <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text"> |
| 5350 | </h3> | 5350 | System Backgrounds |
| 5351 | </h3> | ||
| 5352 | <div class="heading-controls"> | ||
| 5353 | <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> | ||
| 5354 | <i class="fa-solid fa-minus"></i> | ||
| 5355 | </button> | ||
| 5356 | <button id="bg_thumb_zoom_in" class="menu_button menu_button_icon" title="Make thumbnails larger" data-i18n="[title]Make thumbnails larger"> | ||
| 5357 | <i class="fa-solid fa-plus"></i> | ||
| 5358 | </button> | ||
| 5359 | </div> | ||
| 5360 | </div> | ||
| 5351 | <div id="bg_menu_content" class="bg_list"> | 5361 | <div id="bg_menu_content" class="bg_list"> |
| 5352 | </div> | 5362 | </div> |
| 5353 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> | 5363 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> |
| @@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren | |||
| 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat } from './utils.js'; | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce } from './utils.js'; |
| 7 | import { debounce_timeout } from './constants.js'; | ||
| 7 | import { t } from './i18n.js'; | 8 | import { t } from './i18n.js'; |
| 8 | import { Popup } from './popup.js'; | 9 | import { Popup } from './popup.js'; |
| 9 | 10 | ||
| @@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY | |||
| 15 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); | 16 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); |
| 16 | const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; | 17 | const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; |
| 17 | 18 | ||
| 19 | const THUMBNAIL_COLUMNS_MIN = 2; | ||
| 20 | const THUMBNAIL_COLUMNS_MAX = 8; | ||
| 21 | const THUMBNAIL_COLUMNS_DEFAULT_DESKTOP = 5; | ||
| 22 | const THUMBNAIL_COLUMNS_DEFAULT_MOBILE = 3; | ||
| 23 | |||
| 18 | /** | 24 | /** |
| 19 | * Storage for frontend-generated background thumbnails. | 25 | * Storage for frontend-generated background thumbnails. |
| 20 | * This is used to store thumbnails for backgrounds that cannot be generated on the server. | 26 | * This is used to store thumbnails for backgrounds that cannot be generated on the server. |
| @@ -45,6 +51,53 @@ export let background_settings = { | |||
| 45 | animation: false, | 51 | animation: false, |
| 46 | }; | 52 | }; |
| 47 | 53 | ||
| 54 | /** | ||
| 55 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. | ||
| 56 | * @param {object} imageData - Data for the image (filename, isCustom). | ||
| 57 | * @returns {HTMLElement} The created thumbnail element. | ||
| 58 | */ | ||
| 59 | function createThumbnailElement(imageData) { | ||
| 60 | const bg = imageData.filename; | ||
| 61 | const isCustom = imageData.isCustom; | ||
| 62 | |||
| 63 | const thumbnail = $('#background_template .bg_example').clone(); | ||
| 64 | |||
| 65 | const clipper = document.createElement('div'); | ||
| 66 | clipper.className = 'thumbnail-clipper lazy-load-background'; | ||
| 67 | clipper.style.backgroundImage = PLACEHOLDER_IMAGE; | ||
| 68 | |||
| 69 | const titleElement = thumbnail.find('.BGSampleTitle'); | ||
| 70 | clipper.appendChild(titleElement.get(0)); | ||
| 71 | thumbnail.append(clipper); | ||
| 72 | |||
| 73 | const url = generateUrlParameter(bg, isCustom); | ||
| 74 | const title = isCustom ? bg.split('/').pop() : bg; | ||
| 75 | const friendlyTitle = title.slice(0, title.lastIndexOf('.')); | ||
| 76 | |||
| 77 | thumbnail.attr('title', title); | ||
| 78 | thumbnail.attr('bgfile', bg); | ||
| 79 | thumbnail.attr('custom', String(isCustom)); | ||
| 80 | thumbnail.data('url', url); | ||
| 81 | titleElement.text(friendlyTitle); | ||
| 82 | |||
| 83 | return thumbnail.get(0); | ||
| 84 | } | ||
| 85 | |||
| 86 | /** | ||
| 87 | * Applies the thumbnail column count to the CSS and updates button states. | ||
| 88 | * @param {number} count - The number of columns to display. | ||
| 89 | */ | ||
| 90 | function applyThumbnailColumns(count) { | ||
| 91 | const newCount = Math.max(THUMBNAIL_COLUMNS_MIN, Math.min(count, THUMBNAIL_COLUMNS_MAX)); | ||
| 92 | background_settings.thumbnailColumns = newCount; | ||
| 93 | document.documentElement.style.setProperty('--bg-thumb-columns', newCount.toString()); | ||
| 94 | |||
| 95 | $('#bg_thumb_zoom_in').prop('disabled', newCount <= THUMBNAIL_COLUMNS_MIN); | ||
| 96 | $('#bg_thumb_zoom_out').prop('disabled', newCount >= THUMBNAIL_COLUMNS_MAX); | ||
| 97 | |||
| 98 | saveSettingsDebounced(); | ||
| 99 | } | ||
| 100 | |||
| 48 | export function loadBackgroundSettings(settings) { | 101 | export function loadBackgroundSettings(settings) { |
| 49 | let backgroundSettings = settings.background; | 102 | let backgroundSettings = settings.background; |
| 50 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { | 103 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { |
| @@ -56,6 +109,16 @@ export function loadBackgroundSettings(settings) { | |||
| 56 | if (!Object.hasOwn(backgroundSettings, 'animation')) { | 109 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 57 | backgroundSettings.animation = false; | 110 | backgroundSettings.animation = false; |
| 58 | } | 111 | } |
| 112 | |||
| 113 | // If a value is already saved, use it. Otherwise, determine default based on screen size. | ||
| 114 | let columns = backgroundSettings.thumbnailColumns; | ||
| 115 | if (!columns) { | ||
| 116 | const isNarrowScreen = window.matchMedia('(max-width: 480px)').matches; | ||
| 117 | columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; | ||
| 118 | } | ||
| 119 | background_settings.thumbnailColumns = columns; | ||
| 120 | applyThumbnailColumns(background_settings.thumbnailColumns); | ||
| 121 | |||
| 59 | setBackground(backgroundSettings.name, backgroundSettings.url); | 122 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 60 | setFittingClass(backgroundSettings.fitting); | 123 | setFittingClass(backgroundSettings.fitting); |
| 61 | $('#background_fitting').val(backgroundSettings.fitting); | 124 | $('#background_fitting').val(backgroundSettings.fitting); |
| @@ -75,7 +138,7 @@ async function forceSetBackground(backgroundInfo) { | |||
| 75 | list.push(bg); | 138 | list.push(bg); |
| 76 | chat_metadata[LIST_METADATA_KEY] = list; | 139 | chat_metadata[LIST_METADATA_KEY] = list; |
| 77 | saveMetadataDebounced(); | 140 | saveMetadataDebounced(); |
| 78 | await getChatBackgroundsList(); | 141 | renderChatBackgrounds(); |
| 79 | highlightNewBackground(bg); | 142 | highlightNewBackground(bg); |
| 80 | highlightLockedBackground(); | 143 | highlightLockedBackground(); |
| 81 | } | 144 | } |
| @@ -88,28 +151,10 @@ async function onChatChanged() { | |||
| 88 | unsetCustomBackground(); | 151 | unsetCustomBackground(); |
| 89 | } | 152 | } |
| 90 | 153 | ||
| 91 | await getChatBackgroundsList(); | 154 | renderChatBackgrounds(); |
| 92 | highlightLockedBackground(); | 155 | highlightLockedBackground(); |
| 93 | } | 156 | } |
| 94 | 157 | ||
| 95 | async function getChatBackgroundsList() { | ||
| 96 | const list = chat_metadata[LIST_METADATA_KEY]; | ||
| 97 | const listEmpty = !Array.isArray(list) || list.length === 0; | ||
| 98 | |||
| 99 | $('#bg_custom_content').empty(); | ||
| 100 | $('#bg_chat_hint').toggle(listEmpty); | ||
| 101 | |||
| 102 | if (listEmpty) { | ||
| 103 | return; | ||
| 104 | } | ||
| 105 | |||
| 106 | for (const bg of list) { | ||
| 107 | const template = await getBackgroundFromTemplate(bg, true); | ||
| 108 | $('#bg_custom_content').append(template); | ||
| 109 | } | ||
| 110 | activateLazyLoader(); | ||
| 111 | } | ||
| 112 | |||
| 113 | function getBackgroundPath(fileUrl) { | 158 | function getBackgroundPath(fileUrl) { |
| 114 | return `backgrounds/${encodeURIComponent(fileUrl)}`; | 159 | return `backgrounds/${encodeURIComponent(fileUrl)}`; |
| 115 | } | 160 | } |
| @@ -257,7 +302,7 @@ async function onCopyToSystemBackgroundClick(e) { | |||
| 257 | const index = list.indexOf(bgNames.oldBg); | 302 | const index = list.indexOf(bgNames.oldBg); |
| 258 | list.splice(index, 1); | 303 | list.splice(index, 1); |
| 259 | saveMetadataDebounced(); | 304 | saveMetadataDebounced(); |
| 260 | await getChatBackgroundsList(); | 305 | renderChatBackgrounds(); |
| 261 | } | 306 | } |
| 262 | 307 | ||
| 263 | /** | 308 | /** |
| @@ -382,17 +427,21 @@ async function onDeleteBackgroundClick(e) { | |||
| 382 | list.splice(index, 1); | 427 | list.splice(index, 1); |
| 383 | } | 428 | } |
| 384 | 429 | ||
| 385 | const siblingSelector = '.bg_example:not(#form_bg_download)'; | 430 | if (bg === background_settings.name) { |
| 386 | const nextBg = bgToDelete.next(siblingSelector); | 431 | const siblingSelector = '.bg_example'; |
| 387 | const prevBg = bgToDelete.prev(siblingSelector); | 432 | const nextBg = bgToDelete.next(siblingSelector); |
| 388 | const anyBg = $(siblingSelector); | 433 | const prevBg = bgToDelete.prev(siblingSelector); |
| 389 | 434 | ||
| 390 | if (nextBg.length > 0) { | 435 | if (nextBg.length > 0) { |
| 391 | nextBg.trigger('click'); | 436 | nextBg.trigger('click'); |
| 392 | } else if (prevBg.length > 0) { | 437 | } else if (prevBg.length > 0) { |
| 393 | prevBg.trigger('click'); | 438 | prevBg.trigger('click'); |
| 394 | } else { | 439 | } else { |
| 395 | $(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click'); | 440 | const anyOtherBg = $('.bg_example').not(bgToDelete).first(); |
| 441 | if (anyOtherBg.length > 0) { | ||
| 442 | anyOtherBg.trigger('click'); | ||
| 443 | } | ||
| 444 | } | ||
| 396 | } | 445 | } |
| 397 | 446 | ||
| 398 | bgToDelete.remove(); | 447 | bgToDelete.remove(); |
| @@ -404,7 +453,7 @@ async function onDeleteBackgroundClick(e) { | |||
| 404 | } | 453 | } |
| 405 | 454 | ||
| 406 | if (isCustom) { | 455 | if (isCustom) { |
| 407 | await getChatBackgroundsList(); | 456 | renderChatBackgrounds(); |
| 408 | saveMetadataDebounced(); | 457 | saveMetadataDebounced(); |
| 409 | } | 458 | } |
| 410 | } | 459 | } |
| @@ -445,6 +494,47 @@ async function autoBackgroundCommand() { | |||
| 445 | return ''; | 494 | return ''; |
| 446 | } | 495 | } |
| 447 | 496 | ||
| 497 | /** | ||
| 498 | * Renders the system backgrounds gallery. | ||
| 499 | * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | ||
| 500 | */ | ||
| 501 | function renderSystemBackgrounds(backgrounds) { | ||
| 502 | const sourceList = backgrounds || []; | ||
| 503 | const container = $('#bg_menu_content'); | ||
| 504 | container.empty(); | ||
| 505 | |||
| 506 | if (sourceList.length === 0) return; | ||
| 507 | |||
| 508 | sourceList.forEach(bg => { | ||
| 509 | const imageData = { filename: bg, isCustom: false }; | ||
| 510 | const thumbnail = createThumbnailElement(imageData); | ||
| 511 | container.append(thumbnail); | ||
| 512 | }); | ||
| 513 | |||
| 514 | activateLazyLoader(); | ||
| 515 | } | ||
| 516 | |||
| 517 | /** | ||
| 518 | * Renders the chat-specific (custom) backgrounds gallery. | ||
| 519 | * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | ||
| 520 | */ | ||
| 521 | function renderChatBackgrounds(backgrounds) { | ||
| 522 | const sourceList = backgrounds ?? (chat_metadata[LIST_METADATA_KEY] || []); | ||
| 523 | const container = $('#bg_custom_content'); | ||
| 524 | container.empty(); | ||
| 525 | $('#bg_chat_hint').toggle(!sourceList.length); | ||
| 526 | |||
| 527 | if (sourceList.length === 0) return; | ||
| 528 | |||
| 529 | sourceList.forEach(bg => { | ||
| 530 | const imageData = { filename: bg, isCustom: true }; | ||
| 531 | const thumbnail = createThumbnailElement(imageData); | ||
| 532 | container.append(thumbnail); | ||
| 533 | }); | ||
| 534 | |||
| 535 | activateLazyLoader(); | ||
| 536 | } | ||
| 537 | |||
| 448 | export async function getBackgrounds() { | 538 | export async function getBackgrounds() { |
| 449 | const response = await fetch('/api/backgrounds/all', { | 539 | const response = await fetch('/api/backgrounds/all', { |
| 450 | method: 'POST', | 540 | method: 'POST', |
| @@ -454,12 +544,7 @@ export async function getBackgrounds() { | |||
| 454 | if (response.ok) { | 544 | if (response.ok) { |
| 455 | const { images, config } = await response.json(); | 545 | const { images, config } = await response.json(); |
| 456 | Object.assign(THUMBNAIL_CONFIG, config); | 546 | Object.assign(THUMBNAIL_CONFIG, config); |
| 457 | $('#bg_menu_content').children('div').remove(); | 547 | renderSystemBackgrounds(images); |
| 458 | for (const bg of images) { | ||
| 459 | const template = await getBackgroundFromTemplate(bg, false); | ||
| 460 | $('#bg_menu_content').append(template); | ||
| 461 | } | ||
| 462 | activateLazyLoader(); | ||
| 463 | } | 548 | } |
| 464 | } | 549 | } |
| 465 | 550 | ||
| @@ -481,14 +566,19 @@ function activateLazyLoader() { | |||
| 481 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { | 566 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { |
| 482 | entries.forEach(entry => { | 567 | entries.forEach(entry => { |
| 483 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { | 568 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { |
| 484 | const target = entry.target; | 569 | const clipper = entry.target; |
| 485 | const bg = target.getAttribute('bgfile'); | 570 | const parentThumbnail = clipper.closest('.bg_example'); |
| 486 | const isCustom = target.getAttribute('custom') === 'true'; | 571 | |
| 487 | resolveImageUrl(bg, isCustom) | 572 | if (parentThumbnail) { |
| 488 | .then(url => { target.style.backgroundImage = url; }) | 573 | const bg = parentThumbnail.getAttribute('bgfile'); |
| 489 | .catch(() => { target.style.backgroundImage = PLACEHOLDER_IMAGE; }); | 574 | const isCustom = parentThumbnail.getAttribute('custom') === 'true'; |
| 490 | target.classList.remove('lazy-load-background'); | 575 | resolveImageUrl(bg, isCustom) |
| 491 | observer.unobserve(target); | 576 | .then(url => { clipper.style.backgroundImage = url; }) |
| 577 | .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); | ||
| 578 | } | ||
| 579 | |||
| 580 | clipper.classList.remove('lazy-load-background'); | ||
| 581 | observer.unobserve(clipper); | ||
| 492 | } | 582 | } |
| 493 | }); | 583 | }); |
| 494 | }, options); | 584 | }, options); |
| @@ -529,28 +619,6 @@ async function resolveImageUrl(bg, isCustom) { | |||
| 529 | return `url("${thumbnailUrl}")`; | 619 | return `url("${thumbnailUrl}")`; |
| 530 | } | 620 | } |
| 531 | 621 | ||
| 532 | /** | ||
| 533 | * Instantiates a background template | ||
| 534 | * @param {string} bg Path to background | ||
| 535 | * @param {boolean} isCustom Whether the background is custom | ||
| 536 | * @returns {Promise<JQuery<HTMLElement>>} Background template | ||
| 537 | */ | ||
| 538 | async function getBackgroundFromTemplate(bg, isCustom) { | ||
| 539 | const template = $('#background_template .bg_example').clone(); | ||
| 540 | const url = generateUrlParameter(bg, isCustom); | ||
| 541 | const title = isCustom ? bg.split('/').pop() : bg; | ||
| 542 | const friendlyTitle = title.slice(0, title.lastIndexOf('.')); | ||
| 543 | |||
| 544 | template.attr('title', title); | ||
| 545 | template.attr('bgfile', bg); | ||
| 546 | template.attr('custom', String(isCustom)); | ||
| 547 | template.data('url', url); | ||
| 548 | template.addClass('lazy-load-background'); | ||
| 549 | template.css('background-image', PLACEHOLDER_IMAGE); | ||
| 550 | template.find('.BGSampleTitle').text(friendlyTitle); | ||
| 551 | return template; | ||
| 552 | } | ||
| 553 | |||
| 554 | async function setBackground(bg, url) { | 622 | async function setBackground(bg, url) { |
| 555 | $('#bg1').css('background-image', url); | 623 | $('#bg1').css('background-image', url); |
| 556 | background_settings.name = bg; | 624 | background_settings.name = bg; |
| @@ -688,17 +756,17 @@ function setFittingClass(fitting) { | |||
| 688 | } | 756 | } |
| 689 | 757 | ||
| 690 | function onBackgroundFilterInput() { | 758 | function onBackgroundFilterInput() { |
| 691 | const filterValue = String($(this).val()).toLowerCase(); | 759 | const filterValue = String($('#bg-filter').val()).toLowerCase(); |
| 692 | $('#bg_menu_content > div').each(function () { | 760 | $('#bg_menu_content > .bg_example, #bg_custom_content > .bg_example').each(function () { |
| 693 | const $bgContent = $(this); | 761 | const $bg = $(this); |
| 694 | if ($bgContent.attr('title').toLowerCase().includes(filterValue)) { | 762 | const title = $bg.attr('title') || ''; |
| 695 | $bgContent.show(); | 763 | const hasMatch = title.toLowerCase().includes(filterValue); |
| 696 | } else { | 764 | $bg.toggle(hasMatch); |
| 697 | $bgContent.hide(); | ||
| 698 | } | ||
| 699 | }); | 765 | }); |
| 700 | } | 766 | } |
| 701 | 767 | ||
| 768 | const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard); | ||
| 769 | |||
| 702 | export function initBackgrounds() { | 770 | export function initBackgrounds() { |
| 703 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); | 771 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 704 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); | 772 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); |
| @@ -715,6 +783,11 @@ export function initBackgrounds() { | |||
| 715 | $context.addClass('mobile-menu-open'); | 783 | $context.addClass('mobile-menu-open'); |
| 716 | } | 784 | } |
| 717 | }) | 785 | }) |
| 786 | .off('blur', '.bg_example.mobile-menu-open').on('blur', '.bg_example.mobile-menu-open', function () { | ||
| 787 | if (!$(this).is(':focus-within')) { | ||
| 788 | $(this).removeClass('mobile-menu-open'); | ||
| 789 | } | ||
| 790 | }) | ||
| 718 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { | 791 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 719 | e.stopPropagation(); | 792 | e.stopPropagation(); |
| 720 | const action = $(this).data('action'); | 793 | const action = $(this).data('action'); |
| @@ -738,9 +811,15 @@ export function initBackgrounds() { | |||
| 738 | } | 811 | } |
| 739 | }); | 812 | }); |
| 740 | 813 | ||
| 814 | $('#bg_thumb_zoom_in').on('click', () => { | ||
| 815 | applyThumbnailColumns(background_settings.thumbnailColumns - 1); | ||
| 816 | }); | ||
| 817 | $('#bg_thumb_zoom_out').on('click', () => { | ||
| 818 | applyThumbnailColumns(background_settings.thumbnailColumns + 1); | ||
| 819 | }); | ||
| 741 | $('#auto_background').on('click', autoBackgroundCommand); | 820 | $('#auto_background').on('click', autoBackgroundCommand); |
| 742 | $('#add_bg_button').on('change', onBackgroundUploadSelected); | 821 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 743 | $('#bg-filter').on('input', onBackgroundFilterInput); | 822 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 744 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 823 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 745 | name: 'lockbg', | 824 | name: 'lockbg', |
| 746 | callback: () => onLockBackgroundClick(new CustomEvent('click')), | 825 | callback: () => onLockBackgroundClick(new CustomEvent('click')), |
| @@ -2940,6 +2940,7 @@ select option:not(:checked) { | |||
| 2940 | text-align: center; | 2940 | text-align: center; |
| 2941 | } | 2941 | } |
| 2942 | 2942 | ||
| 2943 | .menu_button[disabled], | ||
| 2943 | .menu_button.disabled { | 2944 | .menu_button.disabled { |
| 2944 | filter: brightness(75%) grayscale(1); | 2945 | filter: brightness(75%) grayscale(1); |
| 2945 | opacity: 0.5; | 2946 | opacity: 0.5; |
| @@ -3675,8 +3676,8 @@ grammarly-extension { | |||
| 3675 | min-width: calc(1.25em + 12px); | 3676 | min-width: calc(1.25em + 12px); |
| 3676 | } | 3677 | } |
| 3677 | 3678 | ||
| 3678 | .menu_button:not(.disabled):hover, | 3679 | .menu_button:not(.disabled):not([disabled]):hover, |
| 3679 | .menu_button:not(.disabled).active { | 3680 | .menu_button:not(.disabled):not([disabled]).active { |
| 3680 | background-color: var(--white30a); | 3681 | background-color: var(--white30a); |
| 3681 | } | 3682 | } |
| 3682 | 3683 | ||