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>
This commit is contained in:
L
2025-09-16 05:20:06 -05:00
committed by GitHub
parent 33458b86b0
commit eec837cee6
5 changed files with 244 additions and 114 deletions
+53 -27
View File
@@ -88,6 +88,24 @@ body.reduced-motion #bg_custom {
gap: 5px;
}
/* Control buttons in header */
.heading-container-with-controls {
position: relative;
}
.heading-container-with-controls .heading-text {
margin: 10px 0;
}
.heading-container-with-controls .heading-controls {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 5px;
}
#bg-scrollable-content {
flex-grow: 1;
overflow-y: auto;
@@ -95,57 +113,53 @@ body.reduced-motion #bg_custom {
padding: 0 5px 15px;
}
#bg_menu_content,
#bg_custom_content {
display: grid;
gap: 5px;
width: 100%;
grid-template-columns: repeat(var(--bg-thumb-columns, 5), 1fr);
}
#bg-filter {
font-size: calc(var(--mainFontSize) * 0.95);
}
/* Thumbnail Menu & Buttons */
/* Thumbnails */
.bg_example:hover .BGSampleTitle {
opacity: 1;
}
.bg_example .mobile-only-menu-toggle {
display: none;
}
.bg_example.flex-container {
width: 30%;
max-width: 200px;
margin: 5px;
aspect-ratio: 16/9;
.bg_example {
cursor: pointer;
box-shadow: 0 0 7px var(--black50a);
position: relative;
overflow: hidden;
border-radius: 8px;
border: 0px solid transparent;
outline: 2px solid var(--SmartThemeBorderColor);
outline-offset: -1px;
height: auto;
aspect-ratio: 16 / 9;
}
.bg_example.flex-container:focus-visible {
.bg_example:focus-visible {
outline-offset: inherit;
}
.bg_example_img {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background-image: inherit;
background-size: cover;
background-position: center;
}
.bg_example .jg-menu {
display: flex;
position: absolute;
top: 2px;
right: 2px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
gap: 3px;
padding: 3px 5px;
border-radius: 5px;
padding: 3px 3px;
z-index: 3;
backdrop-filter: blur(4px);
border: 1px solid var(--SmartThemeBorderColor);
@@ -168,14 +182,14 @@ body.reduced-motion #bg_custom {
.bg_example .jg-button {
display: flex;
width: 30px;
height: 30px;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
color: white;
padding: 5px;
font-size: 1.1em;
border-radius: 6px;
border-radius: 5px;
transition: background-color var(--animation-duration) ease;
}
@@ -195,6 +209,18 @@ body.reduced-motion #bg_custom {
display: flex;
}
.thumbnail-clipper {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
overflow: hidden;
border-radius: inherit;
background-size: cover;
background-position: center;
}
.bg_example:not([custom="true"]) .jg-copy,
.bg_example[custom="true"] .jg-edit {
display: none;
+18 -4
View File
@@ -21,6 +21,14 @@
display: none;
}
#bg_menu_content, #bg_custom_content {
grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr);
}
.bg_list {
width: unset;
}
.bg_button {
font-size: 15px;
}
@@ -40,6 +48,11 @@
z-index: 4;
}
.bg_example.mobile-menu-open .mobile-only-menu-toggle {
opacity: 0;
pointer-events: none;
}
.bg_example .mobile-only-menu-toggle {
display: flex;
align-items: center;
@@ -57,6 +70,11 @@
backdrop-filter: blur(2px);
}
.bg_example .jg-button {
width: 30px;
height: 30px;
}
#bg-header-controls {
flex-wrap: wrap;
row-gap: 10px;
@@ -429,10 +447,6 @@
.horde_multiple_hint {
display: none;
}
.bg_list {
width: unset;
}
}
/*landscape mode phones and ipads*/
+13 -3
View File
@@ -5345,9 +5345,19 @@
</div>
</div>
<div id="bg-scrollable-content">
<h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter">
System Backgrounds
</h3>
<div class="heading-container-with-controls">
<h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text">
System Backgrounds
</h3>
<div class="heading-controls">
<button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">
<i class="fa-solid fa-minus"></i>
</button>
<button id="bg_thumb_zoom_in" class="menu_button menu_button_icon" title="Make thumbnails larger" data-i18n="[title]Make thumbnails larger">
<i class="fa-solid fa-plus"></i>
</button>
</div>
</div>
<div id="bg_menu_content" class="bg_list">
</div>
<h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter">
+157 -78
View File
@@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren
import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { createThumbnail, flashHighlight, getBase64Async, stringFormat } from './utils.js';
import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce } from './utils.js';
import { debounce_timeout } from './constants.js';
import { t } from './i18n.js';
import { Popup } from './popup.js';
@@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY
const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' });
const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`;
const THUMBNAIL_COLUMNS_MIN = 2;
const THUMBNAIL_COLUMNS_MAX = 8;
const THUMBNAIL_COLUMNS_DEFAULT_DESKTOP = 5;
const THUMBNAIL_COLUMNS_DEFAULT_MOBILE = 3;
/**
* Storage for frontend-generated background thumbnails.
* This is used to store thumbnails for backgrounds that cannot be generated on the server.
@@ -45,6 +51,53 @@ export let background_settings = {
animation: false,
};
/**
* Creates a single thumbnail DOM element. The CSS now handles all sizing.
* @param {object} imageData - Data for the image (filename, isCustom).
* @returns {HTMLElement} The created thumbnail element.
*/
function createThumbnailElement(imageData) {
const bg = imageData.filename;
const isCustom = imageData.isCustom;
const thumbnail = $('#background_template .bg_example').clone();
const clipper = document.createElement('div');
clipper.className = 'thumbnail-clipper lazy-load-background';
clipper.style.backgroundImage = PLACEHOLDER_IMAGE;
const titleElement = thumbnail.find('.BGSampleTitle');
clipper.appendChild(titleElement.get(0));
thumbnail.append(clipper);
const url = generateUrlParameter(bg, isCustom);
const title = isCustom ? bg.split('/').pop() : bg;
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
thumbnail.attr('title', title);
thumbnail.attr('bgfile', bg);
thumbnail.attr('custom', String(isCustom));
thumbnail.data('url', url);
titleElement.text(friendlyTitle);
return thumbnail.get(0);
}
/**
* Applies the thumbnail column count to the CSS and updates button states.
* @param {number} count - The number of columns to display.
*/
function applyThumbnailColumns(count) {
const newCount = Math.max(THUMBNAIL_COLUMNS_MIN, Math.min(count, THUMBNAIL_COLUMNS_MAX));
background_settings.thumbnailColumns = newCount;
document.documentElement.style.setProperty('--bg-thumb-columns', newCount.toString());
$('#bg_thumb_zoom_in').prop('disabled', newCount <= THUMBNAIL_COLUMNS_MIN);
$('#bg_thumb_zoom_out').prop('disabled', newCount >= THUMBNAIL_COLUMNS_MAX);
saveSettingsDebounced();
}
export function loadBackgroundSettings(settings) {
let backgroundSettings = settings.background;
if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) {
@@ -56,6 +109,16 @@ export function loadBackgroundSettings(settings) {
if (!Object.hasOwn(backgroundSettings, 'animation')) {
backgroundSettings.animation = false;
}
// If a value is already saved, use it. Otherwise, determine default based on screen size.
let columns = backgroundSettings.thumbnailColumns;
if (!columns) {
const isNarrowScreen = window.matchMedia('(max-width: 480px)').matches;
columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP;
}
background_settings.thumbnailColumns = columns;
applyThumbnailColumns(background_settings.thumbnailColumns);
setBackground(backgroundSettings.name, backgroundSettings.url);
setFittingClass(backgroundSettings.fitting);
$('#background_fitting').val(backgroundSettings.fitting);
@@ -75,7 +138,7 @@ async function forceSetBackground(backgroundInfo) {
list.push(bg);
chat_metadata[LIST_METADATA_KEY] = list;
saveMetadataDebounced();
await getChatBackgroundsList();
renderChatBackgrounds();
highlightNewBackground(bg);
highlightLockedBackground();
}
@@ -88,28 +151,10 @@ async function onChatChanged() {
unsetCustomBackground();
}
await getChatBackgroundsList();
renderChatBackgrounds();
highlightLockedBackground();
}
async function getChatBackgroundsList() {
const list = chat_metadata[LIST_METADATA_KEY];
const listEmpty = !Array.isArray(list) || list.length === 0;
$('#bg_custom_content').empty();
$('#bg_chat_hint').toggle(listEmpty);
if (listEmpty) {
return;
}
for (const bg of list) {
const template = await getBackgroundFromTemplate(bg, true);
$('#bg_custom_content').append(template);
}
activateLazyLoader();
}
function getBackgroundPath(fileUrl) {
return `backgrounds/${encodeURIComponent(fileUrl)}`;
}
@@ -257,7 +302,7 @@ async function onCopyToSystemBackgroundClick(e) {
const index = list.indexOf(bgNames.oldBg);
list.splice(index, 1);
saveMetadataDebounced();
await getChatBackgroundsList();
renderChatBackgrounds();
}
/**
@@ -382,17 +427,21 @@ async function onDeleteBackgroundClick(e) {
list.splice(index, 1);
}
const siblingSelector = '.bg_example:not(#form_bg_download)';
const nextBg = bgToDelete.next(siblingSelector);
const prevBg = bgToDelete.prev(siblingSelector);
const anyBg = $(siblingSelector);
if (bg === background_settings.name) {
const siblingSelector = '.bg_example';
const nextBg = bgToDelete.next(siblingSelector);
const prevBg = bgToDelete.prev(siblingSelector);
if (nextBg.length > 0) {
nextBg.trigger('click');
} else if (prevBg.length > 0) {
prevBg.trigger('click');
} else {
$(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click');
if (nextBg.length > 0) {
nextBg.trigger('click');
} else if (prevBg.length > 0) {
prevBg.trigger('click');
} else {
const anyOtherBg = $('.bg_example').not(bgToDelete).first();
if (anyOtherBg.length > 0) {
anyOtherBg.trigger('click');
}
}
}
bgToDelete.remove();
@@ -404,7 +453,7 @@ async function onDeleteBackgroundClick(e) {
}
if (isCustom) {
await getChatBackgroundsList();
renderChatBackgrounds();
saveMetadataDebounced();
}
}
@@ -445,6 +494,47 @@ async function autoBackgroundCommand() {
return '';
}
/**
* Renders the system backgrounds gallery.
* @param {string[]} [backgrounds] - Optional filtered list of backgrounds.
*/
function renderSystemBackgrounds(backgrounds) {
const sourceList = backgrounds || [];
const container = $('#bg_menu_content');
container.empty();
if (sourceList.length === 0) return;
sourceList.forEach(bg => {
const imageData = { filename: bg, isCustom: false };
const thumbnail = createThumbnailElement(imageData);
container.append(thumbnail);
});
activateLazyLoader();
}
/**
* Renders the chat-specific (custom) backgrounds gallery.
* @param {string[]} [backgrounds] - Optional filtered list of backgrounds.
*/
function renderChatBackgrounds(backgrounds) {
const sourceList = backgrounds ?? (chat_metadata[LIST_METADATA_KEY] || []);
const container = $('#bg_custom_content');
container.empty();
$('#bg_chat_hint').toggle(!sourceList.length);
if (sourceList.length === 0) return;
sourceList.forEach(bg => {
const imageData = { filename: bg, isCustom: true };
const thumbnail = createThumbnailElement(imageData);
container.append(thumbnail);
});
activateLazyLoader();
}
export async function getBackgrounds() {
const response = await fetch('/api/backgrounds/all', {
method: 'POST',
@@ -454,12 +544,7 @@ export async function getBackgrounds() {
if (response.ok) {
const { images, config } = await response.json();
Object.assign(THUMBNAIL_CONFIG, config);
$('#bg_menu_content').children('div').remove();
for (const bg of images) {
const template = await getBackgroundFromTemplate(bg, false);
$('#bg_menu_content').append(template);
}
activateLazyLoader();
renderSystemBackgrounds(images);
}
}
@@ -481,14 +566,19 @@ function activateLazyLoader() {
lazyLoadObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.target instanceof HTMLElement && entry.isIntersecting) {
const target = entry.target;
const bg = target.getAttribute('bgfile');
const isCustom = target.getAttribute('custom') === 'true';
resolveImageUrl(bg, isCustom)
.then(url => { target.style.backgroundImage = url; })
.catch(() => { target.style.backgroundImage = PLACEHOLDER_IMAGE; });
target.classList.remove('lazy-load-background');
observer.unobserve(target);
const clipper = entry.target;
const parentThumbnail = clipper.closest('.bg_example');
if (parentThumbnail) {
const bg = parentThumbnail.getAttribute('bgfile');
const isCustom = parentThumbnail.getAttribute('custom') === 'true';
resolveImageUrl(bg, isCustom)
.then(url => { clipper.style.backgroundImage = url; })
.catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; });
}
clipper.classList.remove('lazy-load-background');
observer.unobserve(clipper);
}
});
}, options);
@@ -529,28 +619,6 @@ async function resolveImageUrl(bg, isCustom) {
return `url("${thumbnailUrl}")`;
}
/**
* Instantiates a background template
* @param {string} bg Path to background
* @param {boolean} isCustom Whether the background is custom
* @returns {Promise<JQuery<HTMLElement>>} Background template
*/
async function getBackgroundFromTemplate(bg, isCustom) {
const template = $('#background_template .bg_example').clone();
const url = generateUrlParameter(bg, isCustom);
const title = isCustom ? bg.split('/').pop() : bg;
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
template.attr('title', title);
template.attr('bgfile', bg);
template.attr('custom', String(isCustom));
template.data('url', url);
template.addClass('lazy-load-background');
template.css('background-image', PLACEHOLDER_IMAGE);
template.find('.BGSampleTitle').text(friendlyTitle);
return template;
}
async function setBackground(bg, url) {
$('#bg1').css('background-image', url);
background_settings.name = bg;
@@ -688,17 +756,17 @@ function setFittingClass(fitting) {
}
function onBackgroundFilterInput() {
const filterValue = String($(this).val()).toLowerCase();
$('#bg_menu_content > div').each(function () {
const $bgContent = $(this);
if ($bgContent.attr('title').toLowerCase().includes(filterValue)) {
$bgContent.show();
} else {
$bgContent.hide();
}
const filterValue = String($('#bg-filter').val()).toLowerCase();
$('#bg_menu_content > .bg_example, #bg_custom_content > .bg_example').each(function () {
const $bg = $(this);
const title = $bg.attr('title') || '';
const hasMatch = title.toLowerCase().includes(filterValue);
$bg.toggle(hasMatch);
});
}
const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);
export function initBackgrounds() {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
@@ -715,6 +783,11 @@ export function initBackgrounds() {
$context.addClass('mobile-menu-open');
}
})
.off('blur', '.bg_example.mobile-menu-open').on('blur', '.bg_example.mobile-menu-open', function () {
if (!$(this).is(':focus-within')) {
$(this).removeClass('mobile-menu-open');
}
})
.off('click', '.jg-button').on('click', '.jg-button', function (e) {
e.stopPropagation();
const action = $(this).data('action');
@@ -738,9 +811,15 @@ export function initBackgrounds() {
}
});
$('#bg_thumb_zoom_in').on('click', () => {
applyThumbnailColumns(background_settings.thumbnailColumns - 1);
});
$('#bg_thumb_zoom_out').on('click', () => {
applyThumbnailColumns(background_settings.thumbnailColumns + 1);
});
$('#auto_background').on('click', autoBackgroundCommand);
$('#add_bg_button').on('change', onBackgroundUploadSelected);
$('#bg-filter').on('input', onBackgroundFilterInput);
$('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'lockbg',
callback: () => onLockBackgroundClick(new CustomEvent('click')),
+3 -2
View File
@@ -2940,6 +2940,7 @@ select option:not(:checked) {
text-align: center;
}
.menu_button[disabled],
.menu_button.disabled {
filter: brightness(75%) grayscale(1);
opacity: 0.5;
@@ -3675,8 +3676,8 @@ grammarly-extension {
min-width: calc(1.25em + 12px);
}
.menu_button:not(.disabled):hover,
.menu_button:not(.disabled).active {
.menu_button:not(.disabled):not([disabled]):hover,
.menu_button:not(.disabled):not([disabled]).active {
background-color: var(--white30a);
}