Implement persona thumbnails (#4210) * Implement persona thumbnails * Dear Firefox, fix your overzealous image cache * Add cache busting for avatar uploads when overwriting existing files
Signed| @@ -139,7 +139,7 @@ thumbnails: | |||
| 139 | # JPG thumbnail quality (0-100) | 139 | # JPG thumbnail quality (0-100) |
| 140 | quality: 95 | 140 | quality: 95 |
| 141 | # Maximum thumbnail dimensions per type [width, height] | 141 | # Maximum thumbnail dimensions per type [width, height] |
| 142 | dimensions: { 'bg': [160, 90], 'avatar': [96, 144] } | 142 | dimensions: { 'bg': [160, 90], 'avatar': [96, 144], 'persona': [96, 144] } |
| 143 | 143 | ||
| 144 | # PERFORMANCE-RELATED CONFIGURATION | 144 | # PERFORMANCE-RELATED CONFIGURATION |
| 145 | performance: | 145 | performance: |
| @@ -2598,7 +2598,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2598 | mes.swipes = [mes.mes]; | 2598 | mes.swipes = [mes.mes]; |
| 2599 | } | 2599 | } |
| 2600 | 2600 | ||
| 2601 | let avatarImg = getUserAvatar(user_avatar); | 2601 | let avatarImg = getThumbnailUrl('persona', user_avatar); |
| 2602 | const isSystem = mes.is_system; | 2602 | const isSystem = mes.is_system; |
| 2603 | const title = mes.title; | 2603 | const title = mes.title; |
| 2604 | generatedPromptCache = ''; | 2604 | generatedPromptCache = ''; |
| @@ -5575,7 +5575,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul | |||
| 5575 | 5575 | ||
| 5576 | // Lock user avatar to a persona. | 5576 | // Lock user avatar to a persona. |
| 5577 | if (avatar in power_user.personas) { | 5577 | if (avatar in power_user.personas) { |
| 5578 | message.force_avatar = getUserAvatar(avatar); | 5578 | message.force_avatar = getThumbnailUrl('persona', avatar); |
| 5579 | } | 5579 | } |
| 5580 | 5580 | ||
| 5581 | if (messageBias) { | 5581 | if (messageBias) { |
| @@ -7204,7 +7204,7 @@ async function read_avatar_load(input) { | |||
| 7204 | await delay(DEFAULT_SAVE_EDIT_TIMEOUT); | 7204 | await delay(DEFAULT_SAVE_EDIT_TIMEOUT); |
| 7205 | 7205 | ||
| 7206 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); | 7206 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); |
| 7207 | await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { | 7207 | await fetch(getThumbnailUrl('avatar', formData.get('avatar_url').toString()), { |
| 7208 | method: 'GET', | 7208 | method: 'GET', |
| 7209 | cache: 'no-cache', | 7209 | cache: 'no-cache', |
| 7210 | headers: { | 7210 | headers: { |
| @@ -7233,8 +7233,15 @@ async function read_avatar_load(input) { | |||
| 7233 | } | 7233 | } |
| 7234 | } | 7234 | } |
| 7235 | 7235 | ||
| 7236 | export function getThumbnailUrl(type, file) { | 7236 | /** |
| 7237 | return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; | 7237 | * Gets the URL for a thumbnail of a specific type and file. |
| 7238 | * @param {import('../src/endpoints/thumbnails.js').ThumbnailType} type The type of the thumbnail to get | ||
| 7239 | * @param {string} file The file name or path for which to get the thumbnail URL | ||
| 7240 | * @param {boolean} [t=false] Whether to add a cache-busting timestamp to the URL | ||
| 7241 | * @returns {string} The URL for the thumbnail | ||
| 7242 | */ | ||
| 7243 | export function getThumbnailUrl(type, file, t = false) { | ||
| 7244 | return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}${t ? `&t=${Date.now()}` : ''}`; | ||
| 7238 | } | 7245 | } |
| 7239 | 7246 | ||
| 7240 | export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) { | 7247 | export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) { |
| @@ -7274,7 +7281,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | |||
| 7274 | } | 7281 | } |
| 7275 | else if (entity.type === 'persona') { | 7282 | else if (entity.type === 'persona') { |
| 7276 | avatarTemplate.attr({ 'data-pid': id, 'data-chid': null }); | 7283 | avatarTemplate.attr({ 'data-pid': id, 'data-chid': null }); |
| 7277 | avatarTemplate.find('img').attr('src', getUserAvatar(entity.item.avatar)); | 7284 | avatarTemplate.find('img').attr('src', getThumbnailUrl('persona', entity.item.avatar)); |
| 7278 | avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`); | 7285 | avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`); |
| 7279 | } | 7286 | } |
| 7280 | 7287 | ||
| @@ -12147,9 +12154,17 @@ jQuery(async function () { | |||
| 12147 | $('body').append(newElement); | 12154 | $('body').append(newElement); |
| 12148 | newElement.fadeIn(animation_duration); | 12155 | newElement.fadeIn(animation_duration); |
| 12149 | const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`); | 12156 | const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`); |
| 12150 | if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) { //handle user and system avatars | 12157 | if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) { |
| 12151 | zoomedAvatarImgElement.attr('src', thumbURL); | 12158 | //handle user and system avatars |
| 12152 | zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL); | 12159 | const isValidPersona = decodeURIComponent(targetAvatarImg) in power_user.personas; |
| 12160 | if (isValidPersona) { | ||
| 12161 | const personaSrc = getUserAvatar(targetAvatarImg); | ||
| 12162 | zoomedAvatarImgElement.attr('src', personaSrc); | ||
| 12163 | zoomedAvatarImgElement.attr('data-izoomify-url', personaSrc); | ||
| 12164 | } else { | ||
| 12165 | zoomedAvatarImgElement.attr('src', thumbURL); | ||
| 12166 | zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL); | ||
| 12167 | } | ||
| 12153 | } else if (messageElement.attr('is_user') == 'false') { //handle char avatars | 12168 | } else if (messageElement.attr('is_user') == 'false') { //handle char avatars |
| 12154 | zoomedAvatarImgElement.attr('src', avatarSrc); | 12169 | zoomedAvatarImgElement.attr('src', avatarSrc); |
| 12155 | zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc); | 12170 | zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc); |
| @@ -45,6 +45,10 @@ class DataMaidDialog { | |||
| 45 | name: t`Background Thumbnails`, | 45 | name: t`Background Thumbnails`, |
| 46 | description: t`Thumbnails for missing or deleted backgrounds.`, | 46 | description: t`Thumbnails for missing or deleted backgrounds.`, |
| 47 | }, | 47 | }, |
| 48 | personaThumbnails: { | ||
| 49 | name: t`Persona Thumbnails`, | ||
| 50 | description: t`Thumbnails for missing or deleted personas.`, | ||
| 51 | }, | ||
| 48 | chatBackups: { | 52 | chatBackups: { |
| 49 | name: t`Chat Backups`, | 53 | name: t`Chat Backups`, |
| 50 | description: t`Automatically generated chat backups.`, | 54 | description: t`Automatically generated chat backups.`, |
| @@ -37,6 +37,7 @@ import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from ' | |||
| 37 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 37 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 38 | import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; | 38 | import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; |
| 39 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 39 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 40 | import { isFirefox } from './browser-fixes.js'; | ||
| 40 | 41 | ||
| 41 | /** | 42 | /** |
| 42 | * @typedef {object} PersonaConnection A connection between a character and a character or group entity | 43 | * @typedef {object} PersonaConnection A connection between a character and a character or group entity |
| @@ -124,7 +125,7 @@ function reloadUserAvatar(force = false) { | |||
| 124 | } | 125 | } |
| 125 | 126 | ||
| 126 | if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') { | 127 | if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') { |
| 127 | avatarImg.attr('src', getUserAvatar(user_avatar)); | 128 | avatarImg.attr('src', getThumbnailUrl('persona', user_avatar)); |
| 128 | } | 129 | } |
| 129 | }); | 130 | }); |
| 130 | } | 131 | } |
| @@ -179,7 +180,6 @@ function verifyPersonaSearchSortRule() { | |||
| 179 | * @returns {JQuery<HTMLElement>} Avatar block | 180 | * @returns {JQuery<HTMLElement>} Avatar block |
| 180 | */ | 181 | */ |
| 181 | function getUserAvatarBlock(avatarId) { | 182 | function getUserAvatarBlock(avatarId) { |
| 182 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; | ||
| 183 | const template = $('#user_avatar_template .avatar-container').clone(); | 183 | const template = $('#user_avatar_template .avatar-container').clone(); |
| 184 | const personaName = power_user.personas[avatarId]; | 184 | const personaName = power_user.personas[avatarId]; |
| 185 | const personaDescription = power_user.persona_descriptions[avatarId]?.description; | 185 | const personaDescription = power_user.persona_descriptions[avatarId]?.description; |
| @@ -189,10 +189,7 @@ function getUserAvatarBlock(avatarId) { | |||
| 189 | template.attr('data-avatar-id', avatarId); | 189 | template.attr('data-avatar-id', avatarId); |
| 190 | template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId); | 190 | template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId); |
| 191 | template.toggleClass('default_persona', avatarId === power_user.default_persona); | 191 | template.toggleClass('default_persona', avatarId === power_user.default_persona); |
| 192 | let avatarUrl = getUserAvatar(avatarId); | 192 | const avatarUrl = getThumbnailUrl('persona', avatarId, isFirefox()); |
| 193 | if (isFirefox) { | ||
| 194 | avatarUrl += '?t=' + Date.now(); | ||
| 195 | } | ||
| 196 | template.find('img').attr('src', avatarUrl); | 193 | template.find('img').attr('src', avatarUrl); |
| 197 | 194 | ||
| 198 | // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this. | 195 | // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this. |
| @@ -386,6 +383,7 @@ async function changeUserAvatar(e) { | |||
| 386 | const name = formData.get('overwrite_name'); | 383 | const name = formData.get('overwrite_name'); |
| 387 | if (name) { | 384 | if (name) { |
| 388 | await fetch(getUserAvatar(String(name)), { cache: 'no-cache' }); | 385 | await fetch(getUserAvatar(String(name)), { cache: 'no-cache' }); |
| 386 | await fetch(getThumbnailUrl('persona', String(name)), { cache: 'no-cache' }); | ||
| 389 | reloadUserAvatar(true); | 387 | reloadUserAvatar(true); |
| 390 | } | 388 | } |
| 391 | 389 | ||
| @@ -1657,7 +1655,7 @@ async function syncUserNameToPersona() { | |||
| 1657 | for (const mes of chat) { | 1655 | for (const mes of chat) { |
| 1658 | if (mes.is_user) { | 1656 | if (mes.is_user) { |
| 1659 | mes.name = name1; | 1657 | mes.name = name1; |
| 1660 | mes.force_avatar = getUserAvatar(user_avatar); | 1658 | mes.force_avatar = getThumbnailUrl('persona', user_avatar); |
| 1661 | } | 1659 | } |
| 1662 | } | 1660 | } |
| 1663 | 1661 | ||
| @@ -18,6 +18,7 @@ export const USER_DIRECTORY_TEMPLATE = Object.freeze({ | |||
| 18 | thumbnails: 'thumbnails', | 18 | thumbnails: 'thumbnails', |
| 19 | thumbnailsBg: 'thumbnails/bg', | 19 | thumbnailsBg: 'thumbnails/bg', |
| 20 | thumbnailsAvatar: 'thumbnails/avatar', | 20 | thumbnailsAvatar: 'thumbnails/avatar', |
| 21 | thumbnailsPersona: 'thumbnails/persona', | ||
| 21 | worlds: 'worlds', | 22 | worlds: 'worlds', |
| 22 | user: 'user', | 23 | user: 'user', |
| 23 | avatars: 'User Avatars', | 24 | avatars: 'User Avatars', |
| @@ -9,12 +9,13 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |||
| 9 | import { getImages, tryParse } from '../util.js'; | 9 | import { getImages, tryParse } from '../util.js'; |
| 10 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | 10 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; |
| 11 | import { applyAvatarCropResize } from './characters.js'; | 11 | import { applyAvatarCropResize } from './characters.js'; |
| 12 | import { invalidateThumbnail } from './thumbnails.js'; | ||
| 12 | 13 | ||
| 13 | export const router = express.Router(); | 14 | export const router = express.Router(); |
| 14 | 15 | ||
| 15 | router.post('/get', function (request, response) { | 16 | router.post('/get', function (request, response) { |
| 16 | var images = getImages(request.user.directories.avatars); | 17 | const images = getImages(request.user.directories.avatars); |
| 17 | response.send(JSON.stringify(images)); | 18 | response.send(images); |
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | router.post('/delete', getFileNameValidationFunction('avatar'), function (request, response) { | 21 | router.post('/delete', getFileNameValidationFunction('avatar'), function (request, response) { |
| @@ -29,6 +30,7 @@ router.post('/delete', getFileNameValidationFunction('avatar'), function (reques | |||
| 29 | 30 | ||
| 30 | if (fs.existsSync(fileName)) { | 31 | if (fs.existsSync(fileName)) { |
| 31 | fs.unlinkSync(fileName); | 32 | fs.unlinkSync(fileName); |
| 33 | invalidateThumbnail(request.user.directories, 'persona', sanitize(request.body.avatar)); | ||
| 32 | return response.send({ result: 'ok' }); | 34 | return response.send({ result: 'ok' }); |
| 33 | } | 35 | } |
| 34 | 36 | ||
| @@ -44,6 +46,12 @@ router.post('/upload', getFileNameValidationFunction('overwrite_name'), async (r | |||
| 44 | const rawImg = await Jimp.read(pathToUpload); | 46 | const rawImg = await Jimp.read(pathToUpload); |
| 45 | const image = await applyAvatarCropResize(rawImg, crop); | 47 | const image = await applyAvatarCropResize(rawImg, crop); |
| 46 | 48 | ||
| 49 | // Remove previous thumbnail and bust cache if overwriting | ||
| 50 | if (request.body.overwrite_name) { | ||
| 51 | invalidateThumbnail(request.user.directories, 'persona', sanitize(request.body.overwrite_name)); | ||
| 52 | response.setHeader('Clear-Site-Data', '"cache"'); | ||
| 53 | } | ||
| 54 | |||
| 47 | const filename = request.body.overwrite_name || `${Date.now()}.png`; | 55 | const filename = request.body.overwrite_name || `${Date.now()}.png`; |
| 48 | const pathToNewFile = path.join(request.user.directories.avatars, filename); | 56 | const pathToNewFile = path.join(request.user.directories.avatars, filename); |
| 49 | writeFileAtomicSync(pathToNewFile, image); | 57 | writeFileAtomicSync(pathToNewFile, image); |
| @@ -18,6 +18,7 @@ const sha256 = str => crypto.createHash('sha256').update(str).digest('hex'); | |||
| 18 | * @property {string[]} groupChats - List of loose group chats | 18 | * @property {string[]} groupChats - List of loose group chats |
| 19 | * @property {string[]} avatarThumbnails - List of loose avatar thumbnails | 19 | * @property {string[]} avatarThumbnails - List of loose avatar thumbnails |
| 20 | * @property {string[]} backgroundThumbnails - List of loose background thumbnails | 20 | * @property {string[]} backgroundThumbnails - List of loose background thumbnails |
| 21 | * @property {string[]} personaThumbnails - List of loose persona thumbnails | ||
| 21 | * @property {string[]} chatBackups - List of chat backups | 22 | * @property {string[]} chatBackups - List of chat backups |
| 22 | * @property {string[]} settingsBackups - List of settings backups | 23 | * @property {string[]} settingsBackups - List of settings backups |
| 23 | */ | 24 | */ |
| @@ -39,6 +40,7 @@ const sha256 = str => crypto.createHash('sha256').update(str).digest('hex'); | |||
| 39 | * @property {DataMaidSanitizedRecord[]} groupChats - List of sanitized loose group chats | 40 | * @property {DataMaidSanitizedRecord[]} groupChats - List of sanitized loose group chats |
| 40 | * @property {DataMaidSanitizedRecord[]} avatarThumbnails - List of sanitized loose avatar thumbnails | 41 | * @property {DataMaidSanitizedRecord[]} avatarThumbnails - List of sanitized loose avatar thumbnails |
| 41 | * @property {DataMaidSanitizedRecord[]} backgroundThumbnails - List of sanitized loose background thumbnails | 42 | * @property {DataMaidSanitizedRecord[]} backgroundThumbnails - List of sanitized loose background thumbnails |
| 43 | * @property {DataMaidSanitizedRecord[]} personaThumbnails - List of sanitized loose persona thumbnails | ||
| 42 | * @property {DataMaidSanitizedRecord[]} chatBackups - List of sanitized chat backups | 44 | * @property {DataMaidSanitizedRecord[]} chatBackups - List of sanitized chat backups |
| 43 | * @property {DataMaidSanitizedRecord[]} settingsBackups - List of sanitized settings backups | 45 | * @property {DataMaidSanitizedRecord[]} settingsBackups - List of sanitized settings backups |
| 44 | */ | 46 | */ |
| @@ -106,6 +108,7 @@ export class DataMaidService { | |||
| 106 | groupChats: await this.#collectGroupChats(), | 108 | groupChats: await this.#collectGroupChats(), |
| 107 | avatarThumbnails: await this.#collectAvatarThumbnails(), | 109 | avatarThumbnails: await this.#collectAvatarThumbnails(), |
| 108 | backgroundThumbnails: await this.#collectBackgroundThumbnails(), | 110 | backgroundThumbnails: await this.#collectBackgroundThumbnails(), |
| 111 | personaThumbnails: await this.#collectPersonaThumbnails(), | ||
| 109 | chatBackups: await this.#collectChatBackups(), | 112 | chatBackups: await this.#collectChatBackups(), |
| 110 | settingsBackups: await this.#collectSettingsBackups(), | 113 | settingsBackups: await this.#collectSettingsBackups(), |
| 111 | }; | 114 | }; |
| @@ -145,6 +148,7 @@ export class DataMaidService { | |||
| 145 | groupChats: await Promise.all(report.groupChats.map(i => this.#sanitizeRecord(i, false))), | 148 | groupChats: await Promise.all(report.groupChats.map(i => this.#sanitizeRecord(i, false))), |
| 146 | avatarThumbnails: await Promise.all(report.avatarThumbnails.map(i => this.#sanitizeRecord(i, false))), | 149 | avatarThumbnails: await Promise.all(report.avatarThumbnails.map(i => this.#sanitizeRecord(i, false))), |
| 147 | backgroundThumbnails: await Promise.all(report.backgroundThumbnails.map(i => this.#sanitizeRecord(i, false))), | 150 | backgroundThumbnails: await Promise.all(report.backgroundThumbnails.map(i => this.#sanitizeRecord(i, false))), |
| 151 | personaThumbnails: await Promise.all(report.personaThumbnails.map(i => this.#sanitizeRecord(i, false))), | ||
| 148 | chatBackups: await Promise.all(report.chatBackups.map(i => this.#sanitizeRecord(i, false))), | 152 | chatBackups: await Promise.all(report.chatBackups.map(i => this.#sanitizeRecord(i, false))), |
| 149 | settingsBackups: await Promise.all(report.settingsBackups.map(i => this.#sanitizeRecord(i, false))), | 153 | settingsBackups: await Promise.all(report.settingsBackups.map(i => this.#sanitizeRecord(i, false))), |
| 150 | }; | 154 | }; |
| @@ -416,6 +420,34 @@ export class DataMaidService { | |||
| 416 | } | 420 | } |
| 417 | 421 | ||
| 418 | /** | 422 | /** |
| 423 | * Collects loose persona thumbnails from the provided directories. | ||
| 424 | * @returns {Promise<string[]>} List of paths to loose persona thumbnails | ||
| 425 | */ | ||
| 426 | async #collectPersonaThumbnails() { | ||
| 427 | const result = []; | ||
| 428 | |||
| 429 | try { | ||
| 430 | const knownPersonas = new Set(); | ||
| 431 | const personas = await fs.promises.readdir(this.directories.avatars, { withFileTypes: true }); | ||
| 432 | for (const file of personas) { | ||
| 433 | if (file.isFile()) { | ||
| 434 | knownPersonas.add(file.name); | ||
| 435 | } | ||
| 436 | } | ||
| 437 | const personaThumbnails = await fs.promises.readdir(this.directories.thumbnailsPersona, { withFileTypes: true }); | ||
| 438 | for (const file of personaThumbnails) { | ||
| 439 | if (file.isFile() && !knownPersonas.has(file.name)) { | ||
| 440 | result.push(path.join(this.directories.thumbnailsPersona, file.name)); | ||
| 441 | } | ||
| 442 | } | ||
| 443 | } catch (error) { | ||
| 444 | console.error('[Data Maid] Error collecting persona thumbnails:', error); | ||
| 445 | } | ||
| 446 | |||
| 447 | return result; | ||
| 448 | } | ||
| 449 | |||
| 450 | /** | ||
| 419 | * Collects chat backups from the provided directories. | 451 | * Collects chat backups from the provided directories. |
| 420 | * @returns {Promise<string[]>} List of paths to chat backups | 452 | * @returns {Promise<string[]>} List of paths to chat backups |
| 421 | */ | 453 | */ |
| @@ -14,16 +14,21 @@ const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean' | |||
| 14 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number')))); | 14 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number')))); |
| 15 | const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; | 15 | const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; |
| 16 | 16 | ||
| 17 | /** | ||
| 18 | * @typedef {'bg' | 'avatar' | 'persona'} ThumbnailType | ||
| 19 | */ | ||
| 20 | |||
| 17 | /** @type {Record<string, number[]>} */ | 21 | /** @type {Record<string, number[]>} */ |
| 18 | export const dimensions = { | 22 | export const dimensions = { |
| 19 | 'bg': getConfigValue('thumbnails.dimensions.bg', [160, 90]), | 23 | 'bg': getConfigValue('thumbnails.dimensions.bg', [160, 90]), |
| 20 | 'avatar': getConfigValue('thumbnails.dimensions.avatar', [96, 144]), | 24 | 'avatar': getConfigValue('thumbnails.dimensions.avatar', [96, 144]), |
| 25 | 'persona': getConfigValue('thumbnails.dimensions.persona', [96, 144]), | ||
| 21 | }; | 26 | }; |
| 22 | 27 | ||
| 23 | /** | 28 | /** |
| 24 | * Gets a path to thumbnail folder based on the type. | 29 | * Gets a path to thumbnail folder based on the type. |
| 25 | * @param {import('../users.js').UserDirectoryList} directories User directories | 30 | * @param {import('../users.js').UserDirectoryList} directories User directories |
| 26 | * @param {'bg' | 'avatar'} type Thumbnail type | 31 | * @param {ThumbnailType} type Thumbnail type |
| 27 | * @returns {string} Path to the thumbnails folder | 32 | * @returns {string} Path to the thumbnails folder |
| 28 | */ | 33 | */ |
| 29 | function getThumbnailFolder(directories, type) { | 34 | function getThumbnailFolder(directories, type) { |
| @@ -36,6 +41,9 @@ function getThumbnailFolder(directories, type) { | |||
| 36 | case 'avatar': | 41 | case 'avatar': |
| 37 | thumbnailFolder = directories.thumbnailsAvatar; | 42 | thumbnailFolder = directories.thumbnailsAvatar; |
| 38 | break; | 43 | break; |
| 44 | case 'persona': | ||
| 45 | thumbnailFolder = directories.thumbnailsPersona; | ||
| 46 | break; | ||
| 39 | } | 47 | } |
| 40 | 48 | ||
| 41 | return thumbnailFolder; | 49 | return thumbnailFolder; |
| @@ -44,7 +52,7 @@ function getThumbnailFolder(directories, type) { | |||
| 44 | /** | 52 | /** |
| 45 | * Gets a path to the original images folder based on the type. | 53 | * Gets a path to the original images folder based on the type. |
| 46 | * @param {import('../users.js').UserDirectoryList} directories User directories | 54 | * @param {import('../users.js').UserDirectoryList} directories User directories |
| 47 | * @param {'bg' | 'avatar'} type Thumbnail type | 55 | * @param {ThumbnailType} type Thumbnail type |
| 48 | * @returns {string} Path to the original images folder | 56 | * @returns {string} Path to the original images folder |
| 49 | */ | 57 | */ |
| 50 | function getOriginalFolder(directories, type) { | 58 | function getOriginalFolder(directories, type) { |
| @@ -57,6 +65,9 @@ function getOriginalFolder(directories, type) { | |||
| 57 | case 'avatar': | 65 | case 'avatar': |
| 58 | originalFolder = directories.characters; | 66 | originalFolder = directories.characters; |
| 59 | break; | 67 | break; |
| 68 | case 'persona': | ||
| 69 | originalFolder = directories.avatars; | ||
| 70 | break; | ||
| 60 | } | 71 | } |
| 61 | 72 | ||
| 62 | return originalFolder; | 73 | return originalFolder; |
| @@ -65,7 +76,7 @@ function getOriginalFolder(directories, type) { | |||
| 65 | /** | 76 | /** |
| 66 | * Removes the generated thumbnail from the disk. | 77 | * Removes the generated thumbnail from the disk. |
| 67 | * @param {import('../users.js').UserDirectoryList} directories User directories | 78 | * @param {import('../users.js').UserDirectoryList} directories User directories |
| 68 | * @param {'bg' | 'avatar'} type Type of the thumbnail | 79 | * @param {ThumbnailType} type Type of the thumbnail |
| 69 | * @param {string} file Name of the file | 80 | * @param {string} file Name of the file |
| 70 | */ | 81 | */ |
| 71 | export function invalidateThumbnail(directories, type, file) { | 82 | export function invalidateThumbnail(directories, type, file) { |
| @@ -82,7 +93,7 @@ export function invalidateThumbnail(directories, type, file) { | |||
| 82 | /** | 93 | /** |
| 83 | * Generates a thumbnail for the given file. | 94 | * Generates a thumbnail for the given file. |
| 84 | * @param {import('../users.js').UserDirectoryList} directories User directories | 95 | * @param {import('../users.js').UserDirectoryList} directories User directories |
| 85 | * @param {'bg' | 'avatar'} type Type of the thumbnail | 96 | * @param {ThumbnailType} type Type of the thumbnail |
| 86 | * @param {string} file Name of the file | 97 | * @param {string} file Name of the file |
| 87 | * @returns | 98 | * @returns |
| 88 | */ | 99 | */ |
| @@ -188,7 +199,7 @@ router.get('/', async function (request, response) { | |||
| 188 | return response.sendStatus(400); | 199 | return response.sendStatus(400); |
| 189 | } | 200 | } |
| 190 | 201 | ||
| 191 | if (!(type == 'bg' || type == 'avatar')) { | 202 | if (!(type === 'bg' || type === 'avatar' || type === 'persona')) { |
| 192 | return response.sendStatus(400); | 203 | return response.sendStatus(400); |
| 193 | } | 204 | } |
| 194 | 205 | ||
| @@ -71,6 +71,7 @@ const STORAGE_KEYS = { | |||
| 71 | * @property {string} thumbnails - The directory where the thumbnails are stored | 71 | * @property {string} thumbnails - The directory where the thumbnails are stored |
| 72 | * @property {string} thumbnailsBg - The directory where the background thumbnails are stored | 72 | * @property {string} thumbnailsBg - The directory where the background thumbnails are stored |
| 73 | * @property {string} thumbnailsAvatar - The directory where the avatar thumbnails are stored | 73 | * @property {string} thumbnailsAvatar - The directory where the avatar thumbnails are stored |
| 74 | * @property {string} thumbnailsPersona - The directory where the persona thumbnails are stored | ||
| 74 | * @property {string} worlds - The directory where the WI are stored | 75 | * @property {string} worlds - The directory where the WI are stored |
| 75 | * @property {string} user - The directory where the user's public data is stored | 76 | * @property {string} user - The directory where the user's public data is stored |
| 76 | * @property {string} avatars - The directory where the avatars are stored | 77 | * @property {string} avatars - The directory where the avatars are stored |