SillyTavern/public/scripts/tags.js

857 lines
28 KiB
JavaScript
Raw Normal View History

2023-07-20 19:32:15 +02:00
import {
characters,
saveSettingsDebounced,
this_chid,
callPopup,
menu_type,
getCharacters,
entitiesFilter,
2023-11-10 20:56:25 +01:00
printCharacters,
2023-12-02 19:04:51 +01:00
} from '../script.js';
2023-12-03 13:23:20 +01:00
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FilterHelper } from './filters.js';
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js';
import { power_user } from './power-user.js';
2023-07-20 19:32:15 +02:00
export {
tags,
tag_map,
loadTagsSettings,
printTagFilters,
getTagsList,
appendTagToList,
createTagMapFromList,
renameTagKey,
importTags,
sortTags,
compareTagsForSort,
2023-07-20 19:32:15 +02:00
};
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
function getFilterHelper(listSelector) {
2023-09-06 00:47:55 +02:00
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
2023-07-20 19:32:15 +02:00
}
export const tag_filter_types = {
character: 0,
group_member: 1,
};
const ACTIONABLE_TAGS = {
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
2023-11-11 13:53:08 +01:00
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
2023-07-20 19:32:15 +02:00
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
2023-12-02 20:11:06 +01:00
};
2023-07-20 19:32:15 +02:00
const InListActionable = {
2023-12-02 20:11:06 +01:00
};
2023-07-20 19:32:15 +02:00
const DEFAULT_TAGS = [
2023-12-02 19:04:51 +01:00
{ id: uuidv4(), name: 'Plain Text', create_date: Date.now() },
{ id: uuidv4(), name: 'OpenAI', create_date: Date.now() },
{ id: uuidv4(), name: 'W++', create_date: Date.now() },
{ id: uuidv4(), name: 'Boostyle', create_date: Date.now() },
{ id: uuidv4(), name: 'PList', create_date: Date.now() },
{ id: uuidv4(), name: 'AliChat', create_date: Date.now() },
2023-07-20 19:32:15 +02:00
];
let tags = [];
let tag_map = {};
/**
* Applies the favorite filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
*/
function applyFavFilter(filterHelper) {
2023-07-20 19:32:15 +02:00
const isSelected = $(this).hasClass('selected');
const displayFavoritesOnly = !isSelected;
$(this).toggleClass('selected', displayFavoritesOnly);
filterHelper.setFilterData(FILTER_TYPES.FAV, displayFavoritesOnly);
2023-07-20 19:32:15 +02:00
}
/**
* Applies the "is group" filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
*/
function filterByGroups(filterHelper) {
2023-07-20 19:32:15 +02:00
const isSelected = $(this).hasClass('selected');
const displayGroupsOnly = !isSelected;
$(this).toggleClass('selected', displayGroupsOnly);
filterHelper.setFilterData(FILTER_TYPES.GROUP, displayGroupsOnly);
2023-07-20 19:32:15 +02:00
}
function loadTagsSettings(settings) {
tags = settings.tags !== undefined ? settings.tags : DEFAULT_TAGS;
tag_map = settings.tag_map !== undefined ? settings.tag_map : Object.create(null);
}
function renameTagKey(oldKey, newKey) {
const value = tag_map[oldKey];
tag_map[newKey] = value || [];
delete tag_map[oldKey];
saveSettingsDebounced();
}
function createTagMapFromList(listElement, key) {
2023-12-02 19:04:51 +01:00
const tagIds = [...($(listElement).find('.tag').map((_, el) => $(el).attr('id')))];
2023-07-20 19:32:15 +02:00
tag_map[key] = tagIds;
saveSettingsDebounced();
}
function getTagsList(key) {
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [];
return [];
}
return tag_map[key]
.map(x => tags.find(y => y.id === x))
.filter(x => x)
.sort(compareTagsForSort);
2023-07-20 19:32:15 +02:00
}
function getInlineListSelector() {
2023-12-02 19:04:51 +01:00
if (selected_group && menu_type === 'group_edit') {
2023-07-20 19:32:15 +02:00
return `.group_select[grid="${selected_group}"] .tags`;
}
2023-12-02 19:04:51 +01:00
if (this_chid && menu_type === 'character_edit') {
2023-07-20 19:32:15 +02:00
return `.character_select[chid="${this_chid}"] .tags`;
}
return null;
}
function getTagKey() {
2023-12-02 19:04:51 +01:00
if (selected_group && menu_type === 'group_edit') {
2023-07-20 19:32:15 +02:00
return selected_group;
}
2023-12-02 19:04:51 +01:00
if (this_chid && menu_type === 'character_edit') {
2023-07-20 19:32:15 +02:00
return characters[this_chid].avatar;
}
return null;
}
2023-11-04 19:33:15 +01:00
export function getTagKeyForCharacter(characterId = null) {
return characters[characterId]?.avatar;
}
function addTagToMap(tagId, characterId = null) {
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
2023-07-20 19:32:15 +02:00
if (!key) {
return;
}
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [tagId];
}
else {
tag_map[key].push(tagId);
tag_map[key] = tag_map[key].filter(onlyUnique);
2023-07-20 19:32:15 +02:00
}
}
2023-11-04 19:33:15 +01:00
function removeTagFromMap(tagId, characterId = null) {
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
2023-07-20 19:32:15 +02:00
if (!key) {
return;
}
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [];
}
else {
const indexOf = tag_map[key].indexOf(tagId);
tag_map[key].splice(indexOf, 1);
}
}
function findTag(request, resolve, listSelector) {
2023-12-02 19:04:51 +01:00
const skipIds = [...($(listSelector).find('.tag').map((_, el) => $(el).attr('id')))];
2023-09-06 00:47:55 +02:00
const haystack = tags.filter(t => !skipIds.includes(t.id)).map(t => t.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
2023-07-20 19:32:15 +02:00
const needle = request.term.toLowerCase();
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
const result = haystack.filter(x => x.toLowerCase().includes(needle));
if (request.term && !hasExactMatch) {
result.unshift(request.term);
}
resolve(result);
}
function selectTag(event, ui, listSelector) {
let tagName = ui.item.value;
let tag = tags.find(t => t.name === tagName);
// create new tag if it doesn't exist
if (!tag) {
tag = createNewTag(tagName);
}
// unfocus and clear the input
2023-12-02 19:04:51 +01:00
$(event.target).val('').trigger('input');
2023-07-20 19:32:15 +02:00
// add tag to the UI and internal map
appendTagToList(listSelector, tag, { removable: true });
appendTagToList(getInlineListSelector(), tag, { removable: false });
2023-11-04 19:33:15 +01:00
// Optional, check for multiple character ids being present.
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
if (characterIds) {
2023-11-10 20:56:25 +01:00
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
} else {
2023-11-04 19:33:15 +01:00
addTagToMap(tag.id);
}
2023-07-20 19:32:15 +02:00
saveSettingsDebounced();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// need to return false to keep the input clear
return false;
}
function getExistingTags(new_tags) {
let existing_tags = [];
for (let tag of new_tags) {
2023-12-02 20:11:06 +01:00
let foundTag = tags.find(t => t.name.toLowerCase() === tag.toLowerCase());
2023-07-20 19:32:15 +02:00
if (foundTag) {
existing_tags.push(foundTag.name);
}
}
2023-12-02 20:11:06 +01:00
return existing_tags;
2023-07-20 19:32:15 +02:00
}
async function importTags(imported_char) {
2023-12-02 19:04:51 +01:00
let imported_tags = imported_char.tags.filter(t => t !== 'ROOT' && t !== 'TAVERN');
2023-07-20 19:32:15 +02:00
let existingTags = await getExistingTags(imported_tags);
//make this case insensitive
let newTags = imported_tags.filter(t => !existingTags.some(existingTag => existingTag.toLowerCase() === t.toLowerCase()));
2023-12-02 19:04:51 +01:00
let selected_tags = '';
2023-07-20 19:32:15 +02:00
const existingTagsString = existingTags.length ? (': ' + existingTags.join(', ')) : '';
if (newTags.length === 0) {
await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p>`, 'text');
} else {
selected_tags = await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p><p>The following ${newTags.length} new tags will be imported.</p>`, 'input', newTags.join(', '));
}
// @ts-ignore
2023-07-20 19:32:15 +02:00
selected_tags = existingTags.concat(selected_tags.split(','));
// @ts-ignore
2023-12-02 19:04:51 +01:00
selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== '');
2023-07-20 19:32:15 +02:00
//Anti-troll measure
if (selected_tags.length > 15) {
selected_tags = selected_tags.slice(0, 15);
}
for (let tagName of selected_tags) {
let tag = tags.find(t => t.name === tagName);
if (!tag) {
tag = createNewTag(tagName);
}
if (!tag_map[imported_char.avatar].includes(tag.id)) {
tag_map[imported_char.avatar].push(tag.id);
console.debug('added tag to map', tag, imported_char.name);
}
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
saveSettingsDebounced();
await getCharacters();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// need to return false to keep the input clear
return false;
}
function createNewTag(tagName) {
const tag = {
id: uuidv4(),
2023-07-20 19:32:15 +02:00
name: tagName,
is_folder: false,
sort_order: tags.length,
2023-07-20 19:32:15 +02:00
color: '',
2023-09-16 11:37:19 +02:00
color2: '',
2023-11-10 20:56:25 +01:00
create_date: Date.now(),
2023-07-20 19:32:15 +02:00
};
tags.push(tag);
return tag;
}
2023-12-03 13:23:20 +01:00
/**
* Appends a tag to the list element.
* @param {string} listElement List element selector.
* @param {object} tag Tag object.
* @param {TagOptions} options Options for the tag.
* @typedef {{removable?: boolean, selectable?: boolean, action?: function, isGeneralList?: boolean}} TagOptions
* @returns {void}
*/
2023-07-20 19:32:15 +02:00
function appendTagToList(listElement, tag, { removable, selectable, action, isGeneralList }) {
if (!listElement) {
return;
}
let tagElement = $('#tag_template .tag').clone();
tagElement.attr('id', tag.id);
2023-09-16 11:37:19 +02:00
//tagElement.css('color', 'var(--SmartThemeBodyColor)');
2023-07-20 19:32:15 +02:00
tagElement.css('background-color', tag.color);
2023-09-16 11:37:19 +02:00
tagElement.css('color', tag.color2);
2023-07-20 19:32:15 +02:00
tagElement.find('.tag_name').text(tag.name);
2023-12-02 19:04:51 +01:00
const removeButton = tagElement.find('.tag_remove');
2023-07-20 19:32:15 +02:00
removable ? removeButton.show() : removeButton.hide();
if (tag.class) {
tagElement.addClass(tag.class);
}
if (tag.icon) {
tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon);
}
if (tag.excluded && isGeneralList) {
$(tagElement).addClass('excluded');
2023-07-20 19:32:15 +02:00
}
if (selectable) {
tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement));
2023-07-20 19:32:15 +02:00
}
if (action) {
const filter = getFilterHelper($(listElement));
tagElement.on('click', () => action.bind(tagElement)(filter));
2023-07-20 19:32:15 +02:00
tagElement.addClass('actionable');
}
2023-11-11 13:53:08 +01:00
/*if (action && tag.id === 2) {
2023-07-20 19:32:15 +02:00
tagElement.addClass('innerActionable hidden');
2023-11-11 13:53:08 +01:00
}*/
2023-07-20 19:32:15 +02:00
$(listElement).append(tagElement);
}
function onTagFilterClick(listElement) {
2023-07-20 19:32:15 +02:00
let excludeTag;
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
$(this).addClass('excluded');
2023-12-02 20:11:06 +01:00
excludeTag = true;
2023-07-20 19:32:15 +02:00
}
else if ($(this).hasClass('excluded')) {
$(this).removeClass('excluded');
excludeTag = false;
}
else {
$(this).addClass('selected');
}
// Manual undefined check required for three-state boolean
if (excludeTag !== undefined) {
const tagId = $(this).attr('id');
const existingTag = tags.find((tag) => tag.id === tagId);
if (existingTag) {
existingTag.excluded = excludeTag;
saveSettingsDebounced();
}
}
runTagFilters(listElement);
}
function runTagFilters(listElement) {
2023-12-02 19:04:51 +01:00
const tagIds = [...($(listElement).find('.tag.selected:not(.actionable)').map((_, el) => $(el).attr('id')))];
const excludedTagIds = [...($(listElement).find('.tag.excluded:not(.actionable)').map((_, el) => $(el).attr('id')))];
const filterHelper = getFilterHelper($(listElement));
filterHelper.setFilterData(FILTER_TYPES.TAG, { excluded: excludedTagIds, selected: tagIds });
2023-07-20 19:32:15 +02:00
}
function printTagFilters(type = tag_filter_types.character) {
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
2023-12-02 19:04:51 +01:00
const selectedTagIds = [...($(FILTER_SELECTOR).find('.tag.selected').map((_, el) => $(el).attr('id')))];
2023-07-20 19:32:15 +02:00
$(FILTER_SELECTOR).empty();
const characterTagIds = Object.values(tag_map).flat();
const tagsToDisplay = tags
.filter(x => characterTagIds.includes(x.id))
.sort(compareTagsForSort);
2023-07-20 19:32:15 +02:00
for (const tag of Object.values(ACTIONABLE_TAGS)) {
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true });
}
$(FILTER_SELECTOR).find('.actionable').last().addClass('margin-right-10px');
for (const tag of Object.values(InListActionable)) {
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true });
}
for (const tag of tagsToDisplay) {
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: true, isGeneralList: true });
if (tag.excluded) {
runTagFilters(FILTER_SELECTOR);
}
2023-07-20 19:32:15 +02:00
}
for (const tagId of selectedTagIds) {
$(`${FILTER_SELECTOR} .tag[id="${tagId}"]`).trigger('click');
}
}
function onTagRemoveClick(event) {
event.stopPropagation();
2023-12-02 19:04:51 +01:00
const tag = $(this).closest('.tag');
const tagId = tag.attr('id');
2023-11-04 19:33:15 +01:00
// Optional, check for multiple character ids being present.
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
2023-07-20 19:32:15 +02:00
tag.remove();
2023-11-04 19:33:15 +01:00
if (characterIds) {
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
} else {
removeTagFromMap(tagId);
}
2023-07-20 19:32:15 +02:00
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
saveSettingsDebounced();
}
// @ts-ignore
2023-07-20 19:32:15 +02:00
function onTagInput(event) {
let val = $(this).val();
if (tags.find(t => t.name === val)) return;
// @ts-ignore
2023-12-02 19:04:51 +01:00
$(this).autocomplete('search', val);
2023-07-20 19:32:15 +02:00
}
function onTagInputFocus() {
// @ts-ignore
2023-07-20 19:32:15 +02:00
$(this).autocomplete('search', $(this).val());
}
function onCharacterCreateClick() {
2023-12-02 19:04:51 +01:00
$('#tagList').empty();
2023-07-20 19:32:15 +02:00
}
function onGroupCreateClick() {
2023-12-02 19:04:51 +01:00
$('#groupTagList').empty();
2023-07-20 19:32:15 +02:00
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
}
export function applyTagsOnCharacterSelect() {
//clearTagsFilter();
const chid = Number($(this).attr('chid'));
const key = characters[chid].avatar;
const tags = getTagsList(key);
2023-12-02 19:04:51 +01:00
$('#tagList').empty();
2023-07-20 19:32:15 +02:00
for (const tag of tags) {
2023-12-02 19:04:51 +01:00
appendTagToList('#tagList', tag, { removable: true });
2023-07-20 19:32:15 +02:00
}
}
function applyTagsOnGroupSelect() {
//clearTagsFilter();
const key = $(this).attr('grid');
const tags = getTagsList(key);
2023-12-02 19:04:51 +01:00
$('#groupTagList').empty();
2023-07-20 19:32:15 +02:00
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
for (const tag of tags) {
2023-12-02 19:04:51 +01:00
appendTagToList('#groupTagList', tag, { removable: true });
2023-07-20 19:32:15 +02:00
}
}
2023-11-04 19:33:15 +01:00
export function createTagInput(inputSelector, listSelector) {
2023-07-20 19:32:15 +02:00
$(inputSelector)
// @ts-ignore
2023-07-20 19:32:15 +02:00
.autocomplete({
source: (i, o) => findTag(i, o, listSelector),
select: (e, u) => selectTag(e, u, listSelector),
minLength: 0,
})
.focus(onTagInputFocus); // <== show tag list on click
}
function onViewTagsListClick() {
$('#dialogue_popup').addClass('large_dialogue_popup');
2023-11-10 20:56:25 +01:00
const list = $(document.createElement('div'));
list.attr('id', 'tag_view_list');
2023-07-20 19:32:15 +02:00
const everything = Object.values(tag_map).flat();
2023-11-10 20:56:25 +01:00
$(list).append(`
<div class="title_restorable alignItemsBaseline">
<h3>Tag Management</h3>
2023-11-14 23:59:44 +01:00
<div class="flex-container alignItemsBaseline">
<div class="menu_button menu_button_icon tag_view_backup" title="Save your tags to a file">
<i class="fa-solid fa-file-export"></i>
<span data-i18n="Backup">Backup</span>
</div>
<div class="menu_button menu_button_icon tag_view_restore" title="Restore tags from a file">
<i class="fa-solid fa-file-import"></i>
<span data-i18n="Restore">Restore</span>
</div>
<div class="menu_button menu_button_icon tag_view_create" title="Create a new tag">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
<input type="file" id="tag_view_restore_input" hidden accept=".json">
2023-11-10 20:56:25 +01:00
</div>
</div>
<div class="justifyLeft m-b-1">
<small>
Drag the handle to reorder.<br>
${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder.<br>' : '')}
2023-11-10 20:56:25 +01:00
Click on the tag name to edit it.<br>
Click on color box to assign new color.
</small>
</div>`);
2023-07-20 19:32:15 +02:00
const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
list.append(tagContainer);
const sortedTags = sortTags(tags);
// var highestSortOrder = sortedTags.reduce((max, tag) => tag.sort_order !== undefined ? Math.max(max, tag.sort_order) : max, -1);
2023-11-14 23:59:44 +01:00
for (const tag of sortedTags) {
// // For drag&drop to work we need a sort_order defined, so set it but not save. Gets persisted if there are any tag settings changes
// if (tag.sort_order === undefined) {
// tag.sort_order = ++highestSortOrder;
// }
appendViewTagToList(tagContainer, tag, everything);
2023-11-10 20:56:25 +01:00
}
2023-09-16 11:37:19 +02:00
makeTagListDraggable(tagContainer);
2023-11-10 20:56:25 +01:00
callPopup(list, 'text');
}
2023-09-16 11:37:19 +02:00
function makeTagListDraggable(tagContainer) {
const onTagsSort = () => {
tagContainer.find('.tag_view_item').each(function (i, tagElement) {
const id = $(tagElement).attr('id');
const tag = tags.find(x => x.id === id);
// Fix the defined colors, because if there is no color set, they seem to get automatically set to black
// based on the color picker after drag&drop, even if there was no color chosen. We just set them back.
const color = $(tagElement).find('.tagColorPickerHolder .tag-color').attr('color');
const color2 = $(tagElement).find('.tagColorPicker2Holder .tag-color2').attr('color');
if (color === '' || color === undefined) {
tag.color = '';
fixColor('background-color', tag.color);
}
if (color2 === '' || color2 === undefined) {
tag.color2 = '';
fixColor('color', tag.color2);
}
// Update the sort order
tag.sort_order = i;
function fixColor(property, color) {
$(tagElement).find('.tag_view_name').css(property, color);
$(`.tag[id="${id}"]`).css(property, color);
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css(property, color);
}
});
saveSettingsDebounced();
// If the order of tags in display has changed, we need to redraw some UI elements
printCharacters(false);
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
};
// @ts-ignore
$(tagContainer).sortable({
delay: getSortableDelay(),
stop: () => onTagsSort(),
handle: '.drag-handle',
});
}
function sortTags(tags) {
return tags.slice().sort(compareTagsForSort);
}
function compareTagsForSort(a, b) {
if (a.sort_order !== undefined && b.sort_order !== undefined) {
return a.sort_order - b.sort_order;
} else if (a.sort_order !== undefined) {
return -1;
} else if (b.sort_order !== undefined) {
return 1;
} else {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}
}
2023-11-14 23:59:44 +01:00
async function onTagRestoreFileSelect(e) {
const file = e.target.files[0];
if (!file) {
console.log('Tag restore: No file selected.');
return;
}
const data = await parseJsonFile(file);
if (!data) {
2023-11-15 01:09:40 +01:00
toastr.warning('Empty file data', 'Tag restore');
2023-11-14 23:59:44 +01:00
console.log('Tag restore: File data empty.');
return;
}
if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') {
2023-11-15 01:09:40 +01:00
toastr.warning('Invalid file format', 'Tag restore');
2023-11-14 23:59:44 +01:00
console.log('Tag restore: Invalid file format.');
return;
}
const warnings = [];
// Import tags
for (const tag of data.tags) {
if (!tag.id || !tag.name) {
warnings.push(`Tag object is invalid: ${JSON.stringify(tag)}.`);
continue;
}
if (tags.find(x => x.id === tag.id)) {
warnings.push(`Tag with id ${tag.id} already exists.`);
continue;
}
tags.push(tag);
}
// Import tag_map
for (const key of Object.keys(data.tag_map)) {
const tagIds = data.tag_map[key];
if (!Array.isArray(tagIds)) {
warnings.push(`Tag map for key ${key} is invalid: ${JSON.stringify(tagIds)}.`);
continue;
}
// Verify that the key points to a valid character or group.
const characterExists = characters.some(x => String(x.avatar) === String(key));
const groupExists = groups.some(x => String(x.id) === String(key));
if (!characterExists && !groupExists) {
warnings.push(`Tag map key ${key} does not exist.`);
continue;
}
// Get existing tag ids for this key or empty array.
const existingTagIds = tag_map[key] || [];
// Merge existing and new tag ids. Remove duplicates.
tag_map[key] = existingTagIds.concat(tagIds).filter(onlyUnique);
// Verify that all tags exist. Remove tags that don't exist.
tag_map[key] = tag_map[key].filter(x => tags.some(y => String(y.id) === String(x)));
}
if (warnings.length) {
toastr.success('Tags restored with warnings. Check console for details.');
console.warn(`TAG RESTORE REPORT\n====================\n${warnings.join('\n')}`);
} else {
toastr.success('Tags restored successfully.');
}
$('#tag_view_restore_input').val('');
saveSettingsDebounced();
printCharacters(true);
onViewTagsListClick();
}
function onBackupRestoreClick() {
$('#tag_view_restore_input')
.off('change')
.on('change', onTagRestoreFileSelect)
.trigger('click');
}
function onTagsBackupClick() {
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
const filename = `tags_${timestamp}.json`;
const data = {
tags: tags,
tag_map: tag_map,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
download(blob, filename, 'application/json');
}
2023-11-10 20:56:25 +01:00
function onTagCreateClick() {
const tag = createNewTag('New Tag');
appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []);
2023-11-10 20:56:25 +01:00
printCharacters(false);
saveSettingsDebounced();
}
2023-09-16 11:37:19 +02:00
2023-11-10 20:56:25 +01:00
function appendViewTagToList(list, tag, everything) {
const count = everything.filter(x => x == tag.id).length;
const template = $('#tag_view_template .tag_view_item').clone();
template.attr('id', tag.id);
template.find('.tag_view_counter_value').text(count);
template.find('.tag_view_name').text(tag.name);
template.find('.tag_view_name').addClass('tag');
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
template.find('.tag_view_name').css('background-color', tag.color);
template.find('.tag_view_name').css('color', tag.color2);
2023-09-16 11:37:19 +02:00
const tagAsFolderId = tag.id + '-tag-folder';
2023-12-02 19:04:51 +01:00
const colorPickerId = tag.id + '-tag-color';
const colorPicker2Id = tag.id + '-tag-color2';
2023-07-20 19:32:15 +02:00
if (!power_user.bogus_folders) {
template.find('.tag_as_folder').hide();
}
template.find('.tag_as_folder').addClass(tag.is_folder == true ? 'yes_folder' : 'no_folder');
2023-11-10 20:56:25 +01:00
template.find('.tagColorPickerHolder').html(
2023-12-02 21:06:57 +01:00
`<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`,
2023-11-10 20:56:25 +01:00
);
template.find('.tagColorPicker2Holder').html(
2023-12-02 21:06:57 +01:00
`<toolcool-color-picker id="${colorPicker2Id}" color="${tag.color2}" class="tag-color2"></toolcool-color-picker>`,
2023-11-10 20:56:25 +01:00
);
2023-09-16 11:37:19 +02:00
template.find('.tag_as_folder').attr('id', tagAsFolderId);
2023-11-10 20:56:25 +01:00
template.find('.tag-color').attr('id', colorPickerId);
template.find('.tag-color2').attr('id', colorPicker2Id);
2023-09-16 11:37:19 +02:00
2023-11-10 20:56:25 +01:00
list.append(template);
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
setTimeout(function () {
document.querySelector(`.tag-color[id="${colorPickerId}"`).addEventListener('change', (evt) => {
onTagColorize(evt);
});
}, 100);
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
setTimeout(function () {
document.querySelector(`.tag-color2[id="${colorPicker2Id}"`).addEventListener('change', (evt) => {
onTagColorize2(evt);
});
}, 100);
// @ts-ignore
2023-11-10 20:56:25 +01:00
$(colorPickerId).color = tag.color;
// @ts-ignore
2023-11-10 20:56:25 +01:00
$(colorPicker2Id).color = tag.color2;
2023-07-20 19:32:15 +02:00
}
function onTagAsFolderClick() {
const id = $(this).closest('.tag_view_item').attr('id');
const tag = tags.find(x => x.id === id);
// Toggle
tag.is_folder = tag.is_folder != true;
$(`.tag_view_item[id="${id}"] .tag_as_folder`).toggleClass('yes_folder').toggleClass('no_folder');
// If folder display has changed, we have to redraw the character list, otherwise this folders state would not change
printCharacters(true);
saveSettingsDebounced();
}
2023-07-20 19:32:15 +02:00
function onTagDeleteClick() {
2023-12-02 19:04:51 +01:00
if (!confirm('Are you sure?')) {
2023-07-20 19:32:15 +02:00
return;
}
const id = $(this).closest('.tag_view_item').attr('id');
for (const key of Object.keys(tag_map)) {
tag_map[key] = tag_map[key].filter(x => x.id !== id);
}
const index = tags.findIndex(x => x.id === id);
tags.splice(index, 1);
$(`.tag[id="${id}"]`).remove();
$(`.tag_view_item[id="${id}"]`).remove();
2023-11-10 20:56:25 +01:00
printCharacters(false);
2023-07-20 19:32:15 +02:00
saveSettingsDebounced();
}
function onTagRenameInput() {
const id = $(this).closest('.tag_view_item').attr('id');
const newName = $(this).text();
const tag = tags.find(x => x.id === id);
tag.name = newName;
$(`.tag[id="${id}"] .tag_name`).text(newName);
saveSettingsDebounced();
}
function onTagColorize(evt) {
console.debug(evt);
const id = $(evt.target).closest('.tag_view_item').attr('id');
const newColor = evt.detail.rgba;
$(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor);
$(`.tag[id="${id}"]`).css('background-color', newColor);
2023-11-10 20:56:25 +01:00
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('background-color', newColor);
2023-07-20 19:32:15 +02:00
const tag = tags.find(x => x.id === id);
tag.color = newColor;
2023-09-16 11:37:19 +02:00
console.debug(tag);
saveSettingsDebounced();
}
function onTagColorize2(evt) {
console.debug(evt);
const id = $(evt.target).closest('.tag_view_item').attr('id');
const newColor = evt.detail.rgba;
$(evt.target).parent().parent().find('.tag_view_name').css('color', newColor);
$(`.tag[id="${id}"]`).css('color', newColor);
2023-11-10 20:56:25 +01:00
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('color', newColor);
2023-09-16 11:37:19 +02:00
const tag = tags.find(x => x.id === id);
tag.color2 = newColor;
2023-07-20 19:32:15 +02:00
console.debug(tag);
saveSettingsDebounced();
}
function onTagListHintClick() {
console.log($(this));
$(this).toggleClass('selected');
2023-12-02 19:04:51 +01:00
$(this).siblings('.tag:not(.actionable)').toggle(100);
$(this).siblings('.innerActionable').toggleClass('hidden');
2023-07-20 19:32:15 +02:00
}
jQuery(() => {
2023-07-20 19:32:15 +02:00
createTagInput('#tagInput', '#tagList');
createTagInput('#groupTagInput', '#groupTagList');
2023-12-02 19:04:51 +01:00
$(document).on('click', '#rm_button_create', onCharacterCreateClick);
$(document).on('click', '#rm_button_group_chats', onGroupCreateClick);
$(document).on('click', '.character_select', applyTagsOnCharacterSelect);
$(document).on('click', '.group_select', applyTagsOnGroupSelect);
$(document).on('click', '.tag_remove', onTagRemoveClick);
$(document).on('input', '.tag_input', onTagInput);
$(document).on('click', '.tags_view', onViewTagsListClick);
$(document).on('click', '.tag_delete', onTagDeleteClick);
$(document).on('click', '.tag_as_folder', onTagAsFolderClick);
2023-12-02 19:04:51 +01:00
$(document).on('input', '.tag_view_name', onTagRenameInput);
$(document).on('click', '.tag_view_create', onTagCreateClick);
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
2023-07-20 19:32:15 +02:00
});