mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-22 06:57:41 +01:00
Merge branch 'SillyTavern:staging' into staging
This commit is contained in:
commit
1528e2afed
@ -167,6 +167,7 @@ import {
|
||||
flashHighlight,
|
||||
isTrueBoolean,
|
||||
toggleDrawer,
|
||||
isElementInViewport,
|
||||
} from './scripts/utils.js';
|
||||
import { debounce_timeout } from './scripts/constants.js';
|
||||
|
||||
@ -1827,10 +1828,10 @@ export async function replaceCurrentChat() {
|
||||
}
|
||||
}
|
||||
|
||||
export function showMoreMessages() {
|
||||
export function showMoreMessages(messagesToLoad = null) {
|
||||
const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
|
||||
let messageId = Number(firstDisplayedMesId);
|
||||
let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
|
||||
let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id,
|
||||
// so the first "new" message being shown will be the last available message
|
||||
@ -1840,6 +1841,7 @@ export function showMoreMessages() {
|
||||
|
||||
console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
|
||||
const prevHeight = $('#chat').prop('scrollHeight');
|
||||
const isButtonInView = isElementInViewport($('#show_more_messages')[0]);
|
||||
|
||||
while (messageId > 0 && count > 0) {
|
||||
let newMessageId = messageId - 1;
|
||||
@ -1852,8 +1854,10 @@ export function showMoreMessages() {
|
||||
$('#show_more_messages').remove();
|
||||
}
|
||||
|
||||
const newHeight = $('#chat').prop('scrollHeight');
|
||||
$('#chat').scrollTop(newHeight - prevHeight);
|
||||
if (isButtonInView) {
|
||||
const newHeight = $('#chat').prop('scrollHeight');
|
||||
$('#chat').scrollTop(newHeight - prevHeight);
|
||||
}
|
||||
}
|
||||
|
||||
export async function printMessages() {
|
||||
@ -5425,20 +5429,24 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
|
||||
await popup.show();
|
||||
}
|
||||
|
||||
function setInContextMessages(lastmsg, type) {
|
||||
function setInContextMessages(msgInContextCount, type) {
|
||||
$('#chat .mes').removeClass('lastInContext');
|
||||
|
||||
if (type === 'swipe' || type === 'regenerate' || type === 'continue') {
|
||||
lastmsg++;
|
||||
msgInContextCount++;
|
||||
}
|
||||
|
||||
const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsg);
|
||||
const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-msgInContextCount);
|
||||
lastMessageBlock.addClass('lastInContext');
|
||||
|
||||
if (lastMessageBlock.length === 0) {
|
||||
const firstMessageId = getFirstDisplayedMessageId();
|
||||
$(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext');
|
||||
}
|
||||
|
||||
// Update last id to chat. No metadata save on purpose, gets hopefully saved via another call
|
||||
const lastMessageId = Math.max(0, chat.length - msgInContextCount);
|
||||
chat_metadata['lastInContextMessageId'] = lastMessageId;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -7301,7 +7309,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
|
||||
// Set a timeout so multiple flashes don't overlap
|
||||
clearTimeout(importFlashTimeout);
|
||||
importFlashTimeout = setTimeout(function () {
|
||||
if (type === 'char_import' || type === 'char_create') {
|
||||
if (type === 'char_import' || type === 'char_create' || type === 'char_import_no_toast') {
|
||||
// Find the page at which the character is located
|
||||
const avatarFileName = charId;
|
||||
const charData = getEntitiesList({ doFilter: true });
|
||||
@ -8853,24 +8861,61 @@ export async function processDroppedFiles(files, data = new Map()) {
|
||||
'charx',
|
||||
];
|
||||
|
||||
const avatarFileNames = [];
|
||||
for (const file of files) {
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) {
|
||||
const preservedName = data instanceof Map && data.get(file);
|
||||
await importCharacter(file, preservedName);
|
||||
const avatarFileName = await importCharacter(file, { preserveFileName: preservedName });
|
||||
if (avatarFileName !== undefined) {
|
||||
avatarFileNames.push(avatarFileName);
|
||||
}
|
||||
} else {
|
||||
toastr.warning(t`Unsupported file type: ` + file.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarFileNames.length > 0) {
|
||||
await importCharactersTags(avatarFileNames);
|
||||
selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports tags for the given characters
|
||||
* @param {string[]} avatarFileNames character avatar filenames whose tags are to import
|
||||
*/
|
||||
async function importCharactersTags(avatarFileNames) {
|
||||
await getCharacters();
|
||||
for (let i = 0; i < avatarFileNames.length; i++) {
|
||||
if (power_user.tag_import_setting !== tag_import_setting.NONE) {
|
||||
const importedCharacter = characters.find(character => character.avatar === avatarFileNames[i]);
|
||||
await importTags(importedCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the given imported char
|
||||
* @param {string} charId char to select
|
||||
*/
|
||||
function selectImportedChar(charId) {
|
||||
let oldSelectedChar = null;
|
||||
if (this_chid !== undefined) {
|
||||
oldSelectedChar = characters[this_chid].avatar;
|
||||
}
|
||||
select_rm_info('char_import_no_toast', charId, oldSelectedChar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a character from a file.
|
||||
* @param {File} file File to import
|
||||
* @param {string?} preserveFileName Whether to preserve original file name
|
||||
* @returns {Promise<void>}
|
||||
* @param {object} [options] - Options
|
||||
* @param {string} [options.preserveFileName] Whether to preserve original file name
|
||||
* @param {Boolean} [options.importTags=false] Whether to import tags
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function importCharacter(file, preserveFileName = '') {
|
||||
async function importCharacter(file, { preserveFileName = '', importTags = false } = {}) {
|
||||
if (is_group_generating || is_send_press) {
|
||||
toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`);
|
||||
throw new Error('Cannot import character while generating');
|
||||
@ -8906,19 +8951,14 @@ async function importCharacter(file, preserveFileName = '') {
|
||||
if (data.file_name !== undefined) {
|
||||
$('#character_search_bar').val('').trigger('input');
|
||||
|
||||
let oldSelectedChar = null;
|
||||
if (this_chid !== undefined) {
|
||||
oldSelectedChar = characters[this_chid].avatar;
|
||||
}
|
||||
toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`);
|
||||
let avatarFileName = `${data.file_name}.png`;
|
||||
if (importTags) {
|
||||
await importCharactersTags([avatarFileName]);
|
||||
|
||||
await getCharacters();
|
||||
select_rm_info('char_import', data.file_name, oldSelectedChar);
|
||||
if (power_user.tag_import_setting !== tag_import_setting.NONE) {
|
||||
let currentContext = getContext();
|
||||
let avatarFileName = `${data.file_name}.png`;
|
||||
let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
|
||||
await importTags(importedCharacter);
|
||||
selectImportedChar(data.file_name);
|
||||
}
|
||||
return avatarFileName;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10797,8 +10837,17 @@ jQuery(async function () {
|
||||
return;
|
||||
}
|
||||
|
||||
const avatarFileNames = [];
|
||||
for (const file of e.target.files) {
|
||||
await importCharacter(file);
|
||||
const avatarFileName = await importCharacter(file);
|
||||
if (avatarFileName !== undefined) {
|
||||
avatarFileNames.push(avatarFileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarFileNames.length > 0) {
|
||||
await importCharactersTags(avatarFileNames);
|
||||
selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -585,10 +585,12 @@ async function enlargeMessageImage() {
|
||||
const imgHolder = document.createElement('div');
|
||||
imgHolder.classList.add('img_enlarged_holder');
|
||||
imgHolder.append(img);
|
||||
const imgContainer = $('<div><pre><code></code></pre></div>');
|
||||
const imgContainer = $('<div><pre><code class="img_enlarged_title"></code></pre></div>');
|
||||
imgContainer.prepend(imgHolder);
|
||||
imgContainer.addClass('img_enlarged_container');
|
||||
imgContainer.find('code').addClass('txt').text(title);
|
||||
|
||||
const codeTitle = imgContainer.find('.img_enlarged_title');
|
||||
codeTitle.addClass('txt').text(title);
|
||||
const titleEmpty = !title || title.trim().length === 0;
|
||||
imgContainer.find('pre').toggle(!titleEmpty);
|
||||
addCopyToCodeBlocks(imgContainer);
|
||||
@ -598,9 +600,17 @@ async function enlargeMessageImage() {
|
||||
popup.dlg.style.width = 'unset';
|
||||
popup.dlg.style.height = 'unset';
|
||||
|
||||
img.addEventListener('click', () => {
|
||||
img.addEventListener('click', event => {
|
||||
const shouldZoom = !img.classList.contains('zoomed');
|
||||
img.classList.toggle('zoomed', shouldZoom);
|
||||
event.stopPropagation();
|
||||
});
|
||||
codeTitle[0]?.addEventListener('click', event => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
popup.dlg.addEventListener('click', event => {
|
||||
popup.completeCancelled();
|
||||
});
|
||||
|
||||
await popup.show();
|
||||
|
@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
|
||||
import { showLoader } from './loader.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './templates.js';
|
||||
import { delay, isSubsetOf, setValueByPath } from './utils.js';
|
||||
import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js';
|
||||
import { getContext } from './st-context.js';
|
||||
import { isAdmin } from './user.js';
|
||||
import { t } from './i18n.js';
|
||||
@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `/scripts/extensions/${name}/${manifest.css}`;
|
||||
const id = sanitizeSelector(`${name}-css`);
|
||||
|
||||
if ($(`link[id="${name}"]`).length === 0) {
|
||||
if ($(`link[id="${id}"]`).length === 0) {
|
||||
const link = document.createElement('link');
|
||||
link.id = name;
|
||||
link.id = id;
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = url;
|
||||
@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `/scripts/extensions/${name}/${manifest.js}`;
|
||||
const id = sanitizeSelector(`${name}-js`);
|
||||
let ready = false;
|
||||
|
||||
if ($(`script[id="${name}"]`).length === 0) {
|
||||
if ($(`script[id="${id}"]`).length === 0) {
|
||||
const script = document.createElement('script');
|
||||
script.id = name;
|
||||
script.id = id;
|
||||
script.type = 'module';
|
||||
script.src = url;
|
||||
script.async = true;
|
||||
|
@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') {
|
||||
const newElement = $(template);
|
||||
newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
|
||||
newElement.attr('forChar', id);
|
||||
newElement.attr('id', `${id}`);
|
||||
newElement.attr('id', id);
|
||||
newElement.find('.drag-grabber').attr('id', `${id}header`);
|
||||
newElement.find('.dragTitle').text('Image Gallery');
|
||||
//add a div for the gallery
|
||||
|
@ -388,7 +388,7 @@ class AllTalkTtsProvider {
|
||||
}
|
||||
|
||||
async fetchRvcVoiceObjects() {
|
||||
if (this.settings.server_version !== 'v2') {
|
||||
if (this.settings.server_version == 'v2') {
|
||||
console.log('Skipping RVC voices fetch for V1 server');
|
||||
return [];
|
||||
}
|
||||
|
@ -202,10 +202,19 @@ export function getLastMessageId({ exclude_swipe_in_propress = true, filter = nu
|
||||
* @returns {number|null} The ID of the first message in the context
|
||||
*/
|
||||
function getFirstIncludedMessageId() {
|
||||
const index = Number(document.querySelector('.lastInContext')?.getAttribute('mesid'));
|
||||
return chat_metadata['lastInContextMessageId'];
|
||||
}
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return index;
|
||||
/**
|
||||
* Returns the ID of the first displayed message in the chat.
|
||||
*
|
||||
* @returns {number|null} The ID of the first displayed message
|
||||
*/
|
||||
function getFirstDisplayedMessageId() {
|
||||
const mesId = Number(document.querySelector('#chat .mes')?.getAttribute('mesid'));
|
||||
|
||||
if (!isNaN(mesId) && mesId >= 0) {
|
||||
return mesId;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -467,6 +476,7 @@ export function evaluateMacros(content, env, postProcessFn) {
|
||||
{ regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() },
|
||||
{ regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() },
|
||||
{ regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') },
|
||||
{ regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') },
|
||||
{ regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') },
|
||||
{ regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') },
|
||||
{ regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') },
|
||||
|
@ -3962,6 +3962,95 @@ $(document).ready(() => {
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'css-var',
|
||||
/** @param {{to: string, varname: string }} args @param {string} value @returns {string} */
|
||||
callback: (args, value) => {
|
||||
// Map enum to target selector
|
||||
const targetSelector = {
|
||||
chat: '#chat',
|
||||
background: '#bg1',
|
||||
gallery: '#gallery',
|
||||
zoomedAvatar: 'div.zoomed_avatar',
|
||||
}[args.to || 'chat'];
|
||||
|
||||
if (!targetSelector) {
|
||||
toastr.error(`Invalid target: ${args.to}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.varname) {
|
||||
toastr.error('CSS variable name is required');
|
||||
return;
|
||||
}
|
||||
if (!args.varname.startsWith('--')) {
|
||||
toastr.error('CSS variable names must start with "--"');
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = document.querySelectorAll(targetSelector);
|
||||
if (elements.length === 0) {
|
||||
toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
elements.forEach(element => {
|
||||
element.style.setProperty(args.varname, value);
|
||||
});
|
||||
|
||||
console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`);
|
||||
},
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'varname',
|
||||
description: 'CSS variable name (starting with double dashes)',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: true,
|
||||
}),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'to',
|
||||
description: 'The target element to which the CSS variable will be applied',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumList: [
|
||||
new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message),
|
||||
new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image),
|
||||
new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character),
|
||||
new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image),
|
||||
],
|
||||
defaultValue: 'chat',
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'CSS variable value',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: true,
|
||||
}),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets a CSS variable to a specified value on a target element.
|
||||
<br />
|
||||
Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar").
|
||||
Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that.
|
||||
<br /><br />
|
||||
<b>This value will be gone after a page reload!</b>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre>
|
||||
Sets the text color of the chat to red
|
||||
</li>
|
||||
<li>
|
||||
<pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre>
|
||||
Remove the blur from the zoomed avatar
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'movingui',
|
||||
callback: setmovingUIPreset,
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
setCharacterName,
|
||||
setExtensionPrompt,
|
||||
setUserName,
|
||||
showMoreMessages,
|
||||
stopGeneration,
|
||||
substituteParams,
|
||||
system_avatar,
|
||||
@ -1964,6 +1965,39 @@ export function initDefaultSlashCommands() {
|
||||
returns: ARGUMENT_TYPE.BOOLEAN,
|
||||
helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'chat-render',
|
||||
helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
|
||||
callback: (args, number) => {
|
||||
showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
|
||||
if (isTrueBoolean(String(args?.scroll ?? ''))) {
|
||||
$('#chat').scrollTop(0);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'scroll',
|
||||
description: 'scroll to the top after rendering',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'false',
|
||||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number of messages', [ARGUMENT_TYPE.NUMBER], false,
|
||||
),
|
||||
],
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'chat-reload',
|
||||
helpString: 'Reloads the current chat.',
|
||||
callback: async () => {
|
||||
await reloadCurrentChat();
|
||||
return '';
|
||||
},
|
||||
}));
|
||||
|
||||
registerVariableCommands();
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ export const enumIcons = {
|
||||
voice: '🎤',
|
||||
server: '🖥️',
|
||||
popup: '🗔',
|
||||
image: '🖼️',
|
||||
|
||||
true: '✔️',
|
||||
false: '❌',
|
||||
|
@ -28,7 +28,8 @@
|
||||
<li><tt>{{lastUserMessage}}</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
|
||||
<li><tt>{{lastCharMessage}}</tt> – <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>
|
||||
<li><tt>{{lastMessageId}}</tt> – <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> – <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> – <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be run at least once in the current session. Will only be updated on generation.</span></li>
|
||||
<li><tt>{{firstDisplayedMessageId}}</tt> – <span data-i18n="help_macros_firstDisplayedMessageId">the ID of the first message loaded into the visible chat.</span></li>
|
||||
<li><tt>{{currentSwipeId}}</tt> – <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
|
||||
<li><tt>{{lastSwipeId}}</tt> – <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
|
||||
<li><tt>{{reverse:(content)}}</tt> – <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li>
|
||||
|
@ -67,6 +67,16 @@ export function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make string safe for use as a CSS selector.
|
||||
* @param {string} str String to sanitize
|
||||
* @param {string} replacement Replacement for invalid characters
|
||||
* @returns {string} Sanitized string
|
||||
*/
|
||||
export function sanitizeSelector(str, replacement = '_') {
|
||||
return String(str).replace(/[^a-z0-9_-]/ig, replacement);
|
||||
}
|
||||
|
||||
export function isValidUrl(value) {
|
||||
try {
|
||||
new URL(value);
|
||||
|
@ -4799,7 +4799,7 @@ body:not(.sd) .mes_img_swipes {
|
||||
|
||||
.img_enlarged {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
cursor: zoom-in
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user