Support all file formats for replace/update
| @@ -4796,7 +4796,7 @@ | |||
| 4796 | <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx" name="avatar"> | 4796 | <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx" name="avatar"> |
| 4797 | <input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off"> | 4797 | <input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off"> |
| 4798 | </form> | 4798 | </form> |
| 4799 | <input type="file" id="character_replace_file" accept="image/png" name="replace_avatar" hidden> | 4799 | <input type="file" id="character_replace_file" accept=".json, image/png, .yaml, .yml, .charx" name="replace_avatar" hidden> |
| 4800 | </div> | 4800 | </div> |
| 4801 | <div name="Character List Panel" id="rm_characters_block" class="right_menu"> | 4801 | <div name="Character List Panel" id="rm_characters_block" class="right_menu"> |
| 4802 | <div id="charListFixedTop"> | 4802 | <div id="charListFixedTop"> |
| @@ -8449,10 +8449,10 @@ async function connectAPISlash(_, text) { | |||
| 8449 | /** | 8449 | /** |
| 8450 | * Imports supported files dropped into the app window. | 8450 | * Imports supported files dropped into the app window. |
| 8451 | * @param {File[]} files Array of files to process | 8451 | * @param {File[]} files Array of files to process |
| 8452 | * @param {boolean?} preserveFileNames Whether to preserve original file names | 8452 | * @param {Map<File, string>} [data] Extra data to pass to the import function |
| 8453 | * @returns {Promise<void>} | 8453 | * @returns {Promise<void>} |
| 8454 | */ | 8454 | */ |
| 8455 | export async function processDroppedFiles(files, preserveFileNames = false) { | 8455 | export async function processDroppedFiles(files, data = new Map()) { |
| 8456 | const allowedMimeTypes = [ | 8456 | const allowedMimeTypes = [ |
| 8457 | 'application/json', | 8457 | 'application/json', |
| 8458 | 'image/png', | 8458 | 'image/png', |
| @@ -8469,7 +8469,8 @@ export async function processDroppedFiles(files, preserveFileNames = false) { | |||
| 8469 | for (const file of files) { | 8469 | for (const file of files) { |
| 8470 | const extension = file.name.split('.').pop().toLowerCase(); | 8470 | const extension = file.name.split('.').pop().toLowerCase(); |
| 8471 | if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) { | 8471 | if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) { |
| 8472 | await importCharacter(file, preserveFileNames); | 8472 | const preservedName = data instanceof Map && data.get(file); |
| 8473 | await importCharacter(file, preservedName); | ||
| 8473 | } else { | 8474 | } else { |
| 8474 | toastr.warning('Unsupported file type: ' + file.name); | 8475 | toastr.warning('Unsupported file type: ' + file.name); |
| 8475 | } | 8476 | } |
| @@ -8479,10 +8480,10 @@ export async function processDroppedFiles(files, preserveFileNames = false) { | |||
| 8479 | /** | 8480 | /** |
| 8480 | * Imports a character from a file. | 8481 | * Imports a character from a file. |
| 8481 | * @param {File} file File to import | 8482 | * @param {File} file File to import |
| 8482 | * @param {boolean?} preserveFileName Whether to preserve original file name | 8483 | * @param {string?} preserveFileName Whether to preserve original file name |
| 8483 | * @returns {Promise<void>} | 8484 | * @returns {Promise<void>} |
| 8484 | */ | 8485 | */ |
| 8485 | async function importCharacter(file, preserveFileName = false) { | 8486 | async function importCharacter(file, preserveFileName = '') { |
| 8486 | if (is_group_generating || is_send_press) { | 8487 | if (is_group_generating || is_send_press) { |
| 8487 | toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted'); | 8488 | toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted'); |
| 8488 | throw new Error('Cannot import character while generating'); | 8489 | throw new Error('Cannot import character while generating'); |
| @@ -8498,7 +8499,7 @@ async function importCharacter(file, preserveFileName = false) { | |||
| 8498 | const formData = new FormData(); | 8499 | const formData = new FormData(); |
| 8499 | formData.append('avatar', file); | 8500 | formData.append('avatar', file); |
| 8500 | formData.append('file_type', format); | 8501 | formData.append('file_type', format); |
| 8501 | formData.append('preserve_file_name', String(preserveFileName)); | 8502 | if (preserveFileName) formData.append('preserved_name', preserveFileName); |
| 8502 | 8503 | ||
| 8503 | const data = await jQuery.ajax({ | 8504 | const data = await jQuery.ajax({ |
| 8504 | type: 'POST', | 8505 | type: 'POST', |
| @@ -10637,10 +10638,12 @@ jQuery(async function () { | |||
| 10637 | } | 10638 | } |
| 10638 | 10639 | ||
| 10639 | try { | 10640 | try { |
| 10640 | const cloneFile = new File([file], characters[this_chid].avatar, { type: file.type }); | ||
| 10641 | const chatFile = characters[this_chid]['chat']; | 10641 | const chatFile = characters[this_chid]['chat']; |
| 10642 | await processDroppedFiles([cloneFile], true); | 10642 | const data = new Map(); |
| 10643 | data.set(file, characters[this_chid].avatar); | ||
| 10644 | await processDroppedFiles([file], data); | ||
| 10643 | await openCharacterChat(chatFile); | 10645 | await openCharacterChat(chatFile); |
| 10646 | await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'no-cache' }); | ||
| 10644 | } catch { | 10647 | } catch { |
| 10645 | toastr.error('Failed to replace the character card.', 'Something went wrong'); | 10648 | toastr.error('Failed to replace the character card.', 'Something went wrong'); |
| 10646 | } | 10649 | } |
| @@ -499,15 +499,16 @@ function convertWorldInfoToCharacterBook(name, entries) { | |||
| 499 | * Import a character from a YAML file. | 499 | * Import a character from a YAML file. |
| 500 | * @param {string} uploadPath Path to the uploaded file | 500 | * @param {string} uploadPath Path to the uploaded file |
| 501 | * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects | 501 | * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects |
| 502 | * @param {string|undefined} preservedFileName Preserved file name | ||
| 502 | * @returns {Promise<string>} Internal name of the character | 503 | * @returns {Promise<string>} Internal name of the character |
| 503 | */ | 504 | */ |
| 504 | async function importFromYaml(uploadPath, context) { | 505 | async function importFromYaml(uploadPath, context, preservedFileName) { |
| 505 | const fileText = fs.readFileSync(uploadPath, 'utf8'); | 506 | const fileText = fs.readFileSync(uploadPath, 'utf8'); |
| 506 | fs.rmSync(uploadPath); | 507 | fs.rmSync(uploadPath); |
| 507 | const yamlData = yaml.parse(fileText); | 508 | const yamlData = yaml.parse(fileText); |
| 508 | console.log('Importing from YAML'); | 509 | console.log('Importing from YAML'); |
| 509 | yamlData.name = sanitize(yamlData.name); | 510 | yamlData.name = sanitize(yamlData.name); |
| 510 | const fileName = getPngName(yamlData.name, context.request.user.directories); | 511 | const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories); |
| 511 | let char = convertToV2({ | 512 | let char = convertToV2({ |
| 512 | 'name': yamlData.name, | 513 | 'name': yamlData.name, |
| 513 | 'description': yamlData.context ?? '', | 514 | 'description': yamlData.context ?? '', |
| @@ -532,9 +533,10 @@ async function importFromYaml(uploadPath, context) { | |||
| 532 | * @param {string} uploadPath | 533 | * @param {string} uploadPath |
| 533 | * @param {object} params | 534 | * @param {object} params |
| 534 | * @param {import('express').Request} params.request | 535 | * @param {import('express').Request} params.request |
| 536 | * @param {string|undefined} preservedFileName Preserved file name | ||
| 535 | * @returns {Promise<string>} Internal name of the character | 537 | * @returns {Promise<string>} Internal name of the character |
| 536 | */ | 538 | */ |
| 537 | async function importFromCharX(uploadPath, { request }) { | 539 | async function importFromCharX(uploadPath, { request }, preservedFileName) { |
| 538 | const data = fs.readFileSync(uploadPath); | 540 | const data = fs.readFileSync(uploadPath); |
| 539 | fs.rmSync(uploadPath); | 541 | fs.rmSync(uploadPath); |
| 540 | console.log('Importing from CharX'); | 542 | console.log('Importing from CharX'); |
| @@ -567,7 +569,7 @@ async function importFromCharX(uploadPath, { request }) { | |||
| 567 | unsetFavFlag(card); | 569 | unsetFavFlag(card); |
| 568 | card['create_date'] = humanizedISO8601DateTime(); | 570 | card['create_date'] = humanizedISO8601DateTime(); |
| 569 | card.name = sanitize(card.name); | 571 | card.name = sanitize(card.name); |
| 570 | const fileName = getPngName(card.name, request.user.directories); | 572 | const fileName = preservedFileName || getPngName(card.name, request.user.directories); |
| 571 | const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request); | 573 | const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request); |
| 572 | return result ? fileName : ''; | 574 | return result ? fileName : ''; |
| 573 | } | 575 | } |
| @@ -576,9 +578,10 @@ async function importFromCharX(uploadPath, { request }) { | |||
| 576 | * Import a character from a JSON file. | 578 | * Import a character from a JSON file. |
| 577 | * @param {string} uploadPath Path to the uploaded file | 579 | * @param {string} uploadPath Path to the uploaded file |
| 578 | * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects | 580 | * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects |
| 581 | * @param {string|undefined} preservedFileName Preserved file name | ||
| 579 | * @returns {Promise<string>} Internal name of the character | 582 | * @returns {Promise<string>} Internal name of the character |
| 580 | */ | 583 | */ |
| 581 | async function importFromJson(uploadPath, { request }) { | 584 | async function importFromJson(uploadPath, { request }, preservedFileName) { |
| 582 | const data = fs.readFileSync(uploadPath, 'utf8'); | 585 | const data = fs.readFileSync(uploadPath, 'utf8'); |
| 583 | fs.unlinkSync(uploadPath); | 586 | fs.unlinkSync(uploadPath); |
| 584 | 587 | ||
| @@ -590,7 +593,7 @@ async function importFromJson(uploadPath, { request }) { | |||
| 590 | unsetFavFlag(jsonData); | 593 | unsetFavFlag(jsonData); |
| 591 | jsonData = readFromV2(jsonData); | 594 | jsonData = readFromV2(jsonData); |
| 592 | jsonData['create_date'] = humanizedISO8601DateTime(); | 595 | jsonData['create_date'] = humanizedISO8601DateTime(); |
| 593 | const pngName = getPngName(jsonData.data?.name || jsonData.name, request.user.directories); | 596 | const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories); |
| 594 | const char = JSON.stringify(jsonData); | 597 | const char = JSON.stringify(jsonData); |
| 595 | const result = await writeCharacterData(defaultAvatarPath, char, pngName, request); | 598 | const result = await writeCharacterData(defaultAvatarPath, char, pngName, request); |
| 596 | return result ? pngName : ''; | 599 | return result ? pngName : ''; |
| @@ -600,7 +603,7 @@ async function importFromJson(uploadPath, { request }) { | |||
| 600 | if (jsonData.creator_notes) { | 603 | if (jsonData.creator_notes) { |
| 601 | jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', ''); | 604 | jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', ''); |
| 602 | } | 605 | } |
| 603 | const pngName = getPngName(jsonData.name, request.user.directories); | 606 | const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories); |
| 604 | let char = { | 607 | let char = { |
| 605 | 'name': jsonData.name, | 608 | 'name': jsonData.name, |
| 606 | 'description': jsonData.description ?? '', | 609 | 'description': jsonData.description ?? '', |
| @@ -626,7 +629,7 @@ async function importFromJson(uploadPath, { request }) { | |||
| 626 | if (jsonData.creator_notes) { | 629 | if (jsonData.creator_notes) { |
| 627 | jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', ''); | 630 | jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', ''); |
| 628 | } | 631 | } |
| 629 | const pngName = getPngName(jsonData.char_name, request.user.directories); | 632 | const pngName = preservedFileName || getPngName(jsonData.char_name, request.user.directories); |
| 630 | let char = { | 633 | let char = { |
| 631 | 'name': jsonData.char_name, | 634 | 'name': jsonData.char_name, |
| 632 | 'description': jsonData.char_persona ?? '', | 635 | 'description': jsonData.char_persona ?? '', |
| @@ -1089,8 +1092,8 @@ function getPngName(file, directories) { | |||
| 1089 | * @returns {string | undefined} - The preserved name if the request is valid, otherwise undefined | 1092 | * @returns {string | undefined} - The preserved name if the request is valid, otherwise undefined |
| 1090 | */ | 1093 | */ |
| 1091 | function getPreservedName(request) { | 1094 | function getPreservedName(request) { |
| 1092 | return request.body.file_type === 'png' && request.body.preserve_file_name === 'true' && request.file?.originalname | 1095 | return typeof request.body.preserved_name === 'string' && request.body.preserved_name.length > 0 |
| 1093 | ? path.parse(request.file.originalname).name | 1096 | ? path.parse(request.body.preserved_name).name |
| 1094 | : undefined; | 1097 | : undefined; |
| 1095 | } | 1098 | } |
| 1096 | 1099 | ||
| @@ -1123,6 +1126,10 @@ router.post('/import', urlencodedParser, async function (request, response) { | |||
| 1123 | return response.sendStatus(400); | 1126 | return response.sendStatus(400); |
| 1124 | } | 1127 | } |
| 1125 | 1128 | ||
| 1129 | if (preservedFileName) { | ||
| 1130 | invalidateThumbnail(request.user.directories, 'avatar', `${preservedFileName}.png`); | ||
| 1131 | } | ||
| 1132 | |||
| 1126 | response.send({ file_name: fileName }); | 1133 | response.send({ file_name: fileName }); |
| 1127 | } catch (err) { | 1134 | } catch (err) { |
| 1128 | console.log(err); | 1135 | console.log(err); |