-
+
diff --git a/public/script.js b/public/script.js
index 4b5ba72fb..b07ca95be 100644
--- a/public/script.js
+++ b/public/script.js
@@ -172,6 +172,7 @@ import {
importTags,
tag_filter_types,
compareTagsForSort,
+ initTags,
} from './scripts/tags.js';
import {
SECRET_KEYS,
@@ -412,6 +413,7 @@ export const event_types = {
CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
// TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted',
+ CHARACTER_DUPLICATED: 'character_duplicated',
};
export const eventSource = new EventEmitter();
@@ -864,6 +866,7 @@ async function firstLoadInit() {
getSystemMessages();
sendSystemMessage(system_message_types.WELCOME);
initLocales();
+ initTags();
await getUserAvatars(true, user_avatar);
await getCharacters();
await getBackgrounds();
@@ -1237,7 +1240,7 @@ function getCharacterBlock(item, id) {
const template = $('#character_template .character_select').clone();
template.attr({ 'chid': id, 'id': `CharID${id}` });
template.find('img').attr('src', this_avatar).attr('alt', item.name);
- template.find('.avatar').attr('title', `[Character] ${item.name}`);
+ template.find('.avatar').attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`);
template.find('.ch_name').text(item.name).attr('title', `[Character] ${item.name}`);
if (power_user.show_card_avatar_urls) {
template.find('.ch_avatar_url').text(item.avatar);
@@ -1855,6 +1858,7 @@ function insertSVGIcon(mes, extra) {
function getMessageFromTemplate({
mesId,
+ swipeId,
characterName,
isUser,
avatarImg,
@@ -1872,6 +1876,7 @@ function getMessageFromTemplate({
const mes = messageTemplate.clone();
mes.attr({
'mesid': mesId,
+ 'swipeid': swipeId,
'ch_name': characterName,
'is_user': isUser,
'is_system': !!isSystem,
@@ -2018,6 +2023,7 @@ function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true
let params = {
mesId: forceId ?? chat.length - 1,
+ swipeId: mes.swipe_id ?? 0,
characterName: mes.name,
isUser: mes.is_user,
avatarImg: avatarImg,
@@ -4320,6 +4326,8 @@ async function DupeChar() {
});
if (response.ok) {
toastr.success('Character Duplicated');
+ const data = await response.json();
+ await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
getCharacters();
}
}
diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js
index eb69f279b..50a2ba478 100644
--- a/public/scripts/BulkEditOverlay.js
+++ b/public/scripts/BulkEditOverlay.js
@@ -48,16 +48,24 @@ class CharacterContextMenu {
* Duplicate one or more characters
*
* @param characterId
- * @returns {Promise
}
+ * @returns {Promise}
*/
static duplicate = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
+ const body = { avatar_url: character.avatar };
- return fetch('/api/characters/duplicate', {
+ const result = await fetch('/api/characters/duplicate', {
method: 'POST',
headers: getRequestHeaders(),
- body: JSON.stringify({ avatar_url: character.avatar }),
+ body: JSON.stringify(body),
});
+
+ if (!result.ok) {
+ throw new Error('Character not duplicated');
+ }
+
+ const data = await result.json();
+ await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
};
/**
diff --git a/public/scripts/tags.js b/public/scripts/tags.js
index 2615d87b3..a995a52f2 100644
--- a/public/scripts/tags.js
+++ b/public/scripts/tags.js
@@ -8,6 +8,8 @@ import {
entitiesFilter,
printCharacters,
buildAvatarList,
+ eventSource,
+ event_types,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js';
@@ -132,7 +134,7 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity
}
if (subForEntity !== undefined && subForEntity.type === 'tag') {
- entities = filterTagSubEntities(subForEntity.item, entities, { filterHidden : filterHidden });
+ entities = filterTagSubEntities(subForEntity.item, entities, { filterHidden: filterHidden });
}
return entities;
@@ -1140,7 +1142,7 @@ function onClearAllFiltersClick() {
// We have to manually go through the elements and unfilter by clicking...
// Thankfully nearly all filter controls are three-state-toggles
const filterTags = $('.rm_tag_controls .rm_tag_filter').find('.tag');
- for(const tag of filterTags) {
+ for (const tag of filterTags) {
const toggleState = $(tag).attr('data-toggle-state');
if (toggleState !== undefined && !isFilterState(toggleState ?? FILTER_STATES.UNDEFINED, FILTER_STATES.UNDEFINED)) {
toggleTagThreeState($(tag), { stateOverride: FILTER_STATES.UNDEFINED, simulateClick: true });
@@ -1151,7 +1153,17 @@ function onClearAllFiltersClick() {
$('#character_search_bar').val('').trigger('input');
}
-jQuery(() => {
+/**
+ * Copy tags from one character to another.
+ * @param {{oldAvatar: string, newAvatar: string}} data Event data
+ */
+function copyTags(data) {
+ const prevTagMap = tag_map[data.oldAvatar] || [];
+ const newTagMap = tag_map[data.newAvatar] || [];
+ tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
+}
+
+export function initTags() {
createTagInput('#tagInput', '#tagList');
createTagInput('#groupTagInput', '#groupTagList');
@@ -1168,5 +1180,5 @@ jQuery(() => {
$(document).on('click', '.tag_view_create', onTagCreateClick);
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
-});
-
+ eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
+}
diff --git a/public/style.css b/public/style.css
index 3f54d5cec..6e000651d 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1416,7 +1416,7 @@ input[type="file"] {
}
.extension_token_counter {
- font-size: calc(var(--mainFontSize) * 0.9);
+ font-size: calc(var(--mainFontSize) * 0.875);
width: 100%;
text-align: right;
margin-bottom: 5px;
diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js
index e2063def3..613f88c79 100644
--- a/src/endpoints/characters.js
+++ b/src/endpoints/characters.js
@@ -1008,7 +1008,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
fs.copyFileSync(filename, newFilename);
console.log(`${filename} was copied to ${newFilename}`);
- response.sendStatus(200);
+ response.send({ path: path.parse(newFilename).base });
}
catch (error) {
console.error(error);