|
import { |
|
eventSource, |
|
this_chid, |
|
characters, |
|
getRequestHeaders, |
|
event_types, |
|
} from '../../../script.js'; |
|
import { groups, selected_group } from '../../group-chats.js'; |
|
import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename } from '../../utils.js'; |
|
import { loadMovingUIState } from '../../power-user.js'; |
|
import { dragElement } from '../../RossAscends-mods.js'; |
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
|
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
|
import { DragAndDropHandler } from '../../dragdrop.js'; |
|
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
|
import { t, translate } from '../../i18n.js'; |
|
|
|
const extensionName = 'gallery'; |
|
const extensionFolderPath = `scripts/extensions/${extensionName}/`; |
|
let firstTime = true; |
|
|
|
|
|
let thumbnailHeight = 150; |
|
let paginationVisiblePages = 10; |
|
let paginationMaxLinesPerPage = 2; |
|
let galleryMaxRows = 3; |
|
|
|
|
|
$('#movingDivs').on('click', '.dragClose', function () { |
|
const relatedId = $(this).data('related-id'); |
|
if (!relatedId) return; |
|
$(`#movingDivs > .draggable[id="${relatedId}"]`).remove(); |
|
}); |
|
|
|
const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved'; |
|
|
|
const mutationObserver = new MutationObserver((mutations) => { |
|
mutations.forEach((mutation) => { |
|
mutation.removedNodes.forEach((node) => { |
|
if (node instanceof HTMLElement && node.tagName === 'DIV' && node.id === 'gallery') { |
|
eventSource.emit(CUSTOM_GALLERY_REMOVED_EVENT); |
|
} |
|
}); |
|
}); |
|
}); |
|
|
|
mutationObserver.observe(document.body, { |
|
childList: true, |
|
subtree: false, |
|
}); |
|
|
|
const SORT = Object.freeze({ |
|
NAME_ASC: { value: 'nameAsc', field: 'name', order: 'asc', label: t`Sort By: Name (A-Z)` }, |
|
NAME_DESC: { value: 'nameDesc', field: 'name', order: 'desc', label: t`Sort By: Name (Z-A)` }, |
|
DATE_ASC: { value: 'dateAsc', field: 'date', order: 'asc', label: t`Sort By: Date (Oldest First)` }, |
|
DATE_DESC: { value: 'dateDesc', field: 'date', order: 'desc', label: t`Sort By: Date (Newest First)` }, |
|
}); |
|
|
|
const defaultSettings = Object.freeze({ |
|
folders: {}, |
|
sort: SORT.DATE_ASC.value, |
|
}); |
|
|
|
|
|
|
|
|
|
function initSettings() { |
|
let shouldSave = false; |
|
const context = SillyTavern.getContext(); |
|
if (!context.extensionSettings.gallery) { |
|
context.extensionSettings.gallery = structuredClone(defaultSettings); |
|
shouldSave = true; |
|
} |
|
for (const key of Object.keys(defaultSettings)) { |
|
if (!Object.hasOwn(context.extensionSettings.gallery, key)) { |
|
context.extensionSettings.gallery[key] = structuredClone(defaultSettings[key]); |
|
shouldSave = true; |
|
} |
|
} |
|
if (shouldSave) { |
|
context.saveSettingsDebounced(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function getGalleryFolder(char) { |
|
return SillyTavern.getContext().extensionSettings.gallery.folders[char?.avatar] ?? char?.name; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function getGalleryItems(url) { |
|
const sortValue = getSortOrder(); |
|
const sortObj = Object.values(SORT).find(it => it.value === sortValue) ?? SORT.DATE_ASC; |
|
const response = await fetch('/api/images/list', { |
|
method: 'POST', |
|
headers: getRequestHeaders(), |
|
body: JSON.stringify({ |
|
folder: url, |
|
sortField: sortObj.field, |
|
sortOrder: sortObj.order, |
|
}), |
|
}); |
|
|
|
url = await getSanitizedFilename(url); |
|
|
|
const data = await response.json(); |
|
const items = data.map((file) => ({ |
|
src: `user/images/${url}/${file}`, |
|
srct: `user/images/${url}/${file}`, |
|
title: '', |
|
})); |
|
|
|
return items; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function getGalleryFolders() { |
|
try { |
|
const response = await fetch('/api/images/folders', { |
|
method: 'POST', |
|
headers: getRequestHeaders(), |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP error. Status: ${response.status}`); |
|
} |
|
const data = await response.json(); |
|
return data; |
|
} catch (error) { |
|
console.error('Failed to fetch gallery folders:', error); |
|
return []; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setSortOrder(order) { |
|
const context = SillyTavern.getContext(); |
|
context.extensionSettings.gallery.sort = order; |
|
context.saveSettingsDebounced(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getSortOrder() { |
|
return SillyTavern.getContext().extensionSettings.gallery.sort ?? defaultSettings.sort; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function initGallery(items, url) { |
|
const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`; |
|
const gallery = $('#dragGallery'); |
|
gallery.addClass(nonce); |
|
gallery.nanogallery2({ |
|
'items': items, |
|
thumbnailWidth: 'auto', |
|
thumbnailHeight: thumbnailHeight, |
|
paginationVisiblePages: paginationVisiblePages, |
|
paginationMaxLinesPerPage: paginationMaxLinesPerPage, |
|
galleryMaxRows: galleryMaxRows, |
|
galleryPaginationTopButtons: false, |
|
galleryNavigationOverlayButtons: true, |
|
galleryTheme: { |
|
navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' }, |
|
navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, |
|
navigationFilter: { color: '#ddd', background: '#111', colorSelected: '#fff', backgroundSelected: '#111', borderRadius: '4px' }, |
|
navigationPagination: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, |
|
thumbnail: { background: '#444', backgroundImage: 'linear-gradient(315deg, #111 0%, #445 90%)', borderColor: '#000', borderRadius: '0px', labelOpacity: 1, labelBackground: 'rgba(34, 34, 34, 0)', titleColor: '#fff', titleBgColor: 'transparent', titleShadow: '', descriptionColor: '#ccc', descriptionBgColor: 'transparent', descriptionShadow: '', stackBackground: '#aaa' }, |
|
thumbnailIcon: { padding: '5px', color: '#fff', shadow: '' }, |
|
pagination: { background: '#181818', backgroundSelected: '#666', color: '#fff', borderRadius: '2px', shapeBorder: '3px solid var(--SmartThemeQuoteColor)', shapeColor: '#444', shapeSelectedColor: '#aaa' }, |
|
}, |
|
galleryDisplayMode: 'pagination', |
|
fnThumbnailOpen: viewWithDragbox, |
|
fnThumbnailInit: function ( $thumbnail, item) { |
|
if (!item?.src) return; |
|
$thumbnail.attr('title', String(item.src).split('/').pop()); |
|
}, |
|
}); |
|
|
|
const dragDropHandler = new DragAndDropHandler(`#dragGallery.${nonce}`, async (files) => { |
|
if (!Array.isArray(files) || files.length === 0) { |
|
return; |
|
} |
|
|
|
|
|
for (const file of files) { |
|
await uploadFile(file, url); |
|
} |
|
|
|
|
|
const newItems = await getGalleryItems(url); |
|
$('#dragGallery').closest('#gallery').remove(); |
|
await makeMovable(url); |
|
await delay(100); |
|
await initGallery(newItems, url); |
|
}); |
|
|
|
const resizeHandler = function () { |
|
gallery.nanogallery2('resize'); |
|
}; |
|
|
|
eventSource.on('resizeUI', resizeHandler); |
|
|
|
eventSource.once(event_types.CHAT_CHANGED, function () { |
|
gallery.closest('#gallery').remove(); |
|
}); |
|
|
|
eventSource.once(CUSTOM_GALLERY_REMOVED_EVENT, function () { |
|
gallery.nanogallery2('destroy'); |
|
dragDropHandler.destroy(); |
|
eventSource.removeListener('resizeUI', resizeHandler); |
|
}); |
|
|
|
|
|
gallery.css('height', gallery.parent().css('height')); |
|
|
|
|
|
await delay(100); |
|
|
|
gallery.css('height', 'unset'); |
|
|
|
gallery.nanogallery2('resize'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function showCharGallery() { |
|
|
|
if (firstTime) { |
|
await loadFileToDocument( |
|
`${extensionFolderPath}nanogallery2.woff.min.css`, |
|
'css', |
|
); |
|
await loadFileToDocument( |
|
`${extensionFolderPath}jquery.nanogallery2.min.js`, |
|
'js', |
|
); |
|
firstTime = false; |
|
toastr.info('Images can also be found in the folder `user/images`', 'Drag and drop images onto the gallery to upload them', { timeOut: 6000 }); |
|
} |
|
|
|
try { |
|
let url = selected_group || this_chid; |
|
if (!selected_group && this_chid !== undefined) { |
|
url = getGalleryFolder(characters[this_chid]); |
|
} |
|
|
|
const items = await getGalleryItems(url); |
|
|
|
$('#dragGallery').closest('#gallery').remove(); |
|
await makeMovable(url); |
|
await delay(100); |
|
await initGallery(items, url); |
|
} catch (err) { |
|
console.trace(); |
|
console.error(err); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadFile(file, url) { |
|
try { |
|
|
|
const base64Data = await getBase64Async(file); |
|
|
|
|
|
const payload = { |
|
image: base64Data, |
|
ch_name: url, |
|
}; |
|
|
|
const response = await fetch('/api/images/upload', { |
|
method: 'POST', |
|
headers: getRequestHeaders(), |
|
body: JSON.stringify(payload), |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP error! Status: ${response.status}`); |
|
} |
|
|
|
const result = await response.json(); |
|
|
|
toastr.success(t`File uploaded successfully. Saved at: ${result.path}`); |
|
} catch (error) { |
|
console.error('There was an issue uploading the file:', error); |
|
|
|
|
|
toastr.error(t`Failed to upload the file.`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function makeMovable(url) { |
|
console.debug('making new container from template'); |
|
const id = 'gallery'; |
|
const template = $('#generic_draggable_template').html(); |
|
const newElement = $(template); |
|
newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); |
|
newElement.attr('forChar', id); |
|
newElement.attr('id', id); |
|
newElement.find('.drag-grabber').attr('id', `${id}header`); |
|
const dragTitle = newElement.find('.dragTitle'); |
|
dragTitle.addClass('flex-container justifySpaceBetween alignItemsBaseline'); |
|
const titleText = document.createElement('span'); |
|
titleText.textContent = t`Image Gallery`; |
|
dragTitle.append(titleText); |
|
const sortSelect = document.createElement('select'); |
|
sortSelect.classList.add('gallery-sort-select'); |
|
|
|
for (const sort of Object.values(SORT)) { |
|
const option = document.createElement('option'); |
|
option.value = sort.value; |
|
option.textContent = sort.label; |
|
sortSelect.appendChild(option); |
|
} |
|
|
|
sortSelect.addEventListener('change', async () => { |
|
const selectedOption = sortSelect.options[sortSelect.selectedIndex].value; |
|
setSortOrder(selectedOption); |
|
closeButton.trigger('click'); |
|
await showCharGallery(); |
|
}); |
|
|
|
sortSelect.value = getSortOrder(); |
|
dragTitle.append(sortSelect); |
|
|
|
|
|
newElement.addClass('no-scrollbar'); |
|
|
|
|
|
const closeButton = newElement.find('.dragClose'); |
|
closeButton.attr('id', `${id}close`); |
|
closeButton.attr('data-related-id', `${id}`); |
|
|
|
const topBarElement = document.createElement('div'); |
|
topBarElement.classList.add('flex-container', 'alignItemsCenter'); |
|
|
|
const onChangeFolder = async ( e) => { |
|
if (e instanceof KeyboardEvent && e.key !== 'Enter') { |
|
return; |
|
} |
|
|
|
try { |
|
const newUrl = await getSanitizedFilename(galleryFolderInput.value); |
|
updateGalleryFolder(newUrl); |
|
closeButton.trigger('click'); |
|
await showCharGallery(); |
|
toastr.info(t`Gallery folder changed to ${newUrl}`); |
|
galleryFolderInput.value = newUrl; |
|
} catch (error) { |
|
console.error('Failed to change gallery folder:', error); |
|
toastr.error(error?.message || t`Unknown error`, t`Failed to change gallery folder`); |
|
} |
|
}; |
|
|
|
const onRestoreFolder = async () => { |
|
try { |
|
restoreGalleryFolder(); |
|
closeButton.trigger('click'); |
|
await showCharGallery(); |
|
} catch (error) { |
|
console.error('Failed to restore gallery folder:', error); |
|
toastr.error(error?.message || t`Unknown error`, t`Failed to restore gallery folder`); |
|
} |
|
}; |
|
|
|
const galleryFolderInput = document.createElement('input'); |
|
galleryFolderInput.type = 'text'; |
|
galleryFolderInput.placeholder = t`Folder Name`; |
|
galleryFolderInput.title = t`Enter a folder name to change the gallery folder`; |
|
galleryFolderInput.value = url; |
|
galleryFolderInput.classList.add('text_pole', 'gallery-folder-input', 'flex1'); |
|
galleryFolderInput.addEventListener('keyup', onChangeFolder); |
|
|
|
const galleryFolderAccept = document.createElement('div'); |
|
galleryFolderAccept.classList.add('right_menu_button', 'fa-solid', 'fa-check', 'fa-fw'); |
|
galleryFolderAccept.title = t`Change gallery folder`; |
|
galleryFolderAccept.addEventListener('click', onChangeFolder); |
|
|
|
const galleryFolderRestore = document.createElement('div'); |
|
galleryFolderRestore.classList.add('right_menu_button', 'fa-solid', 'fa-recycle', 'fa-fw'); |
|
galleryFolderRestore.title = t`Restore gallery folder`; |
|
galleryFolderRestore.addEventListener('click', onRestoreFolder); |
|
|
|
topBarElement.appendChild(galleryFolderInput); |
|
topBarElement.appendChild(galleryFolderAccept); |
|
topBarElement.appendChild(galleryFolderRestore); |
|
newElement.append(topBarElement); |
|
|
|
|
|
const folders = await getGalleryFolders(); |
|
$(galleryFolderInput) |
|
.autocomplete({ |
|
source: (i, o) => { |
|
const term = i.term.toLowerCase(); |
|
const filtered = folders.filter(f => f.toLowerCase().includes(term)); |
|
o(filtered); |
|
}, |
|
select: (e, u) => { |
|
galleryFolderInput.value = u.item.value; |
|
onChangeFolder(e); |
|
}, |
|
minLength: 0, |
|
}) |
|
.on('focus', () => $(galleryFolderInput).autocomplete('search', '')); |
|
|
|
|
|
newElement.append('<div id="dragGallery"></div>'); |
|
|
|
$('#dragGallery').css('display', 'block'); |
|
|
|
$('#movingDivs').append(newElement); |
|
|
|
loadMovingUIState(); |
|
$(`.draggable[forChar="${id}"]`).css('display', 'block'); |
|
dragElement(newElement); |
|
|
|
$(`.draggable[forChar="${id}"] img`).on('dragstart', (e) => { |
|
console.log('saw drag on avatar!'); |
|
e.preventDefault(); |
|
return false; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function updateGalleryFolder(newUrl) { |
|
if (!newUrl) { |
|
throw new Error('Folder name cannot be empty'); |
|
} |
|
const context = SillyTavern.getContext(); |
|
if (context.groupId) { |
|
throw new Error('Cannot change gallery folder in group chat'); |
|
} |
|
if (context.characterId === undefined) { |
|
throw new Error('Character is not selected'); |
|
} |
|
const avatar = context.characters[context.characterId]?.avatar; |
|
const name = context.characters[context.characterId]?.name; |
|
if (!avatar) { |
|
throw new Error('Character PNG ID is not found'); |
|
} |
|
if (newUrl === name) { |
|
|
|
delete context.extensionSettings.gallery.folders[avatar]; |
|
} else { |
|
|
|
context.extensionSettings.gallery.folders[avatar] = newUrl; |
|
} |
|
context.saveSettingsDebounced(); |
|
} |
|
|
|
|
|
|
|
|
|
function restoreGalleryFolder() { |
|
const context = SillyTavern.getContext(); |
|
if (context.groupId) { |
|
throw new Error('Cannot change gallery folder in group chat'); |
|
} |
|
if (context.characterId === undefined) { |
|
throw new Error('Character is not selected'); |
|
} |
|
const avatar = context.characters[context.characterId]?.avatar; |
|
if (!avatar) { |
|
throw new Error('Character PNG ID is not found'); |
|
} |
|
const existingOverride = context.extensionSettings.gallery.folders[avatar]; |
|
if (!existingOverride) { |
|
throw new Error('No folder override found'); |
|
} |
|
delete context.extensionSettings.gallery.folders[avatar]; |
|
context.saveSettingsDebounced(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function makeDragImg(id, url) { |
|
|
|
const template = document.getElementById('generic_draggable_template'); |
|
|
|
if (!(template instanceof HTMLTemplateElement)) { |
|
console.error('The element is not a <template> tag'); |
|
return; |
|
} |
|
|
|
const newElement = document.importNode(template.content, true); |
|
|
|
|
|
const imgElem = document.createElement('img'); |
|
imgElem.src = url; |
|
let uniqueId = `draggable_${id}`; |
|
const draggableElem = (newElement.querySelector('.draggable')); |
|
if (draggableElem) { |
|
draggableElem.appendChild(imgElem); |
|
|
|
|
|
|
|
let counter = 1; |
|
while (document.getElementById(uniqueId)) { |
|
uniqueId = `draggable_${id}_${counter}`; |
|
counter++; |
|
} |
|
draggableElem.id = uniqueId; |
|
|
|
|
|
draggableElem.style.display = 'block'; |
|
|
|
draggableElem.style.padding = '0'; |
|
|
|
|
|
|
|
const closeButton = (draggableElem.querySelector('.dragClose')); |
|
if (closeButton) { |
|
closeButton.id = `${uniqueId}close`; |
|
closeButton.dataset.relatedId = uniqueId; |
|
} |
|
|
|
|
|
const dragGrabber = draggableElem.querySelector('.drag-grabber'); |
|
if (dragGrabber) { |
|
dragGrabber.id = `${uniqueId}header`; |
|
} |
|
} |
|
|
|
|
|
document.getElementById('movingDivs').appendChild(newElement); |
|
|
|
|
|
const appendedElement = document.getElementById(uniqueId); |
|
if (appendedElement) { |
|
var elmntName = $(appendedElement); |
|
loadMovingUIState(); |
|
dragElement(elmntName); |
|
|
|
|
|
$(`#${uniqueId} img`).on('dragstart', (e) => { |
|
console.log('saw drag on avatar!'); |
|
e.preventDefault(); |
|
return false; |
|
}); |
|
} else { |
|
console.error('Failed to append the template content or retrieve the appended content.'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sanitizeHTMLId(id) { |
|
|
|
id = id.replace(/\s+/g, '-') |
|
.replace(/[^\x00-\x7F]/g, '-') |
|
.replace(/\W/g, ''); |
|
|
|
return id; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function viewWithDragbox(items) { |
|
if (items && items.length > 0) { |
|
const url = items[0].responsiveURL(); |
|
|
|
const id = sanitizeHTMLId(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'))); |
|
makeDragImg(id, url); |
|
} |
|
} |
|
|
|
|
|
|
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
|
name: 'show-gallery', |
|
aliases: ['sg'], |
|
callback: () => { |
|
showCharGallery(); |
|
return ''; |
|
}, |
|
helpString: 'Shows the gallery.', |
|
})); |
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
|
name: 'list-gallery', |
|
aliases: ['lg'], |
|
callback: listGalleryCommand, |
|
returns: 'list of images', |
|
namedArgumentList: [ |
|
SlashCommandNamedArgument.fromProps({ |
|
name: 'char', |
|
description: 'character name', |
|
typeList: [ARGUMENT_TYPE.STRING], |
|
enumProvider: commonEnumProviders.characters('character'), |
|
}), |
|
SlashCommandNamedArgument.fromProps({ |
|
name: 'group', |
|
description: 'group name', |
|
typeList: [ARGUMENT_TYPE.STRING], |
|
enumProvider: commonEnumProviders.characters('group'), |
|
}), |
|
], |
|
helpString: 'List images in the gallery of the current char / group or a specified char / group.', |
|
})); |
|
|
|
async function listGalleryCommand(args) { |
|
try { |
|
let url = args.char ?? (args.group ? groups.find(it => it.name == args.group)?.id : null) ?? (selected_group || this_chid); |
|
if (!args.char && !args.group && !selected_group && this_chid !== undefined) { |
|
url = getGalleryFolder(characters[this_chid]); |
|
} |
|
|
|
const items = await getGalleryItems(url); |
|
return JSON.stringify(items.map(it => it.src)); |
|
|
|
} catch (err) { |
|
console.trace(); |
|
console.error(err); |
|
} |
|
return JSON.stringify([]); |
|
} |
|
|
|
|
|
(function () { |
|
initSettings(); |
|
eventSource.on(event_types.CHARACTER_RENAMED, (oldAvatar, newAvatar) => { |
|
const context = SillyTavern.getContext(); |
|
const galleryFolder = context.extensionSettings.gallery.folders[oldAvatar]; |
|
if (galleryFolder) { |
|
context.extensionSettings.gallery.folders[newAvatar] = galleryFolder; |
|
delete context.extensionSettings.gallery.folders[oldAvatar]; |
|
context.saveSettingsDebounced(); |
|
} |
|
}); |
|
eventSource.on(event_types.CHARACTER_DELETED, (data) => { |
|
const avatar = data?.character?.avatar; |
|
if (!avatar) return; |
|
const context = SillyTavern.getContext(); |
|
delete context.extensionSettings.gallery.folders[avatar]; |
|
context.saveSettingsDebounced(); |
|
}); |
|
eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (selectedOptionId) => { |
|
if (selectedOptionId === 'show_char_gallery') { |
|
showCharGallery(); |
|
} |
|
}); |
|
|
|
|
|
$('#char-management-dropdown').append( |
|
$('<option>', { |
|
id: 'show_char_gallery', |
|
text: translate('Show Gallery'), |
|
}), |
|
); |
|
})(); |
|
|