mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-13 10:42:55 +01:00
Add bulk tagging
This commit is contained in:
parent
545d933e15
commit
c3ff146dd2
@ -65,3 +65,24 @@
|
|||||||
#bulkEditButton.bulk_edit_overlay_active {
|
#bulkEditButton.bulk_edit_overlay_active {
|
||||||
color: var(--golden);
|
color: var(--golden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulk_tag_shadow_popup {
|
||||||
|
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||||
|
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||||
|
background-color: var(--black30a);
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100svh;
|
||||||
|
z-index: 9998;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bulk_tag_shadow_popup #bulk_tag_popup {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
|
||||||
|
width: 100px;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#bulk_tags_div,
|
||||||
#tags_div {
|
#tags_div {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@ -86,10 +87,12 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulkTagsList,
|
||||||
#tagList.tags {
|
#tagList.tags {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulkTagsList,
|
||||||
#tagList .tag {
|
#tagList .tag {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
@ -103,9 +103,9 @@
|
|||||||
<div id="character_context_menu" class="hidden">
|
<div id="character_context_menu" class="hidden">
|
||||||
<ul>
|
<ul>
|
||||||
<li><button id="character_context_menu_favorite">Favorite</button></li>
|
<li><button id="character_context_menu_favorite">Favorite</button></li>
|
||||||
<li><button id="character_context_menu_collection">Collection</button></li>
|
<li><button id="character_context_menu_tag">Tag</button></li>
|
||||||
<li><button id="character_context_menu_persona">To Persona</button></li>
|
|
||||||
<li><button id="character_context_menu_duplicate">Duplicate</button></li>
|
<li><button id="character_context_menu_duplicate">Duplicate</button></li>
|
||||||
|
<li><button id="character_context_menu_persona">Persona</button></li>
|
||||||
<li><button id="character_context_menu_delete">Delete</button></li>
|
<li><button id="character_context_menu_delete">Delete</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
callPopup,
|
callPopup,
|
||||||
characters, deleteCharacter,
|
characters,
|
||||||
|
deleteCharacter,
|
||||||
event_types,
|
event_types,
|
||||||
eventSource,
|
eventSource,
|
||||||
getCharacters,
|
getCharacters,
|
||||||
getRequestHeaders, handleDeleteCharacter, this_chid
|
getRequestHeaders,
|
||||||
|
this_chid
|
||||||
} from "../script.js";
|
} from "../script.js";
|
||||||
import {favsToHotswap} from "./RossAscends-mods.js";
|
import {favsToHotswap} from "./RossAscends-mods.js";
|
||||||
import {convertCharacterToPersona} from "./personas.js";
|
import {convertCharacterToPersona} from "./personas.js";
|
||||||
|
import {createTagInput, getTagKeyForCharacter, tag_map} from "./tags.js";
|
||||||
|
|
||||||
const popupMessage = {
|
const popupMessage = {
|
||||||
deleteChat(characterCount) {
|
deleteChat(characterCount) {
|
||||||
@ -20,9 +23,6 @@ const popupMessage = {
|
|||||||
<span>Also delete the chat files</span>
|
<span>Also delete the chat files</span>
|
||||||
</label><br></b>`;
|
</label><br></b>`;
|
||||||
},
|
},
|
||||||
exportCharacters(characterCount) {
|
|
||||||
return `<h3>Export ${characterCount} characters?</h3>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleFavoriteHighlight = (characterId) => {
|
const toggleFavoriteHighlight = (characterId) => {
|
||||||
@ -30,20 +30,16 @@ const toggleFavoriteHighlight = (characterId) => {
|
|||||||
element.classList.toggle('is_fav');
|
element.classList.toggle('is_fav');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Implement a SingletonPattern, allowing access to the group overlay instance
|
|
||||||
* from everywhere via (new CharacterGroupOverlay())
|
|
||||||
*
|
|
||||||
* @type BulkEditOverlay
|
|
||||||
*/
|
|
||||||
let characterGroupOverlayInstance = null;
|
|
||||||
|
|
||||||
class CharacterGroupOverlayState {
|
class CharacterGroupOverlayState {
|
||||||
static browse = 0;
|
static browse = 0;
|
||||||
static select = 1;
|
static select = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CharacterContextMenu {
|
class CharacterContextMenu {
|
||||||
|
static tag = (selectedCharacters) => {
|
||||||
|
BulkTagPopupHandler.show(selectedCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplicate a character
|
* Duplicate a character
|
||||||
*
|
*
|
||||||
@ -141,13 +137,72 @@ class CharacterContextMenu {
|
|||||||
{id: 'character_context_menu_favorite', callback: characterGroupOverlay.handleContextMenuFavorite},
|
{id: 'character_context_menu_favorite', callback: characterGroupOverlay.handleContextMenuFavorite},
|
||||||
{id: 'character_context_menu_duplicate', callback: characterGroupOverlay.handleContextMenuDuplicate},
|
{id: 'character_context_menu_duplicate', callback: characterGroupOverlay.handleContextMenuDuplicate},
|
||||||
{id: 'character_context_menu_delete', callback: characterGroupOverlay.handleContextMenuDelete},
|
{id: 'character_context_menu_delete', callback: characterGroupOverlay.handleContextMenuDelete},
|
||||||
{id: 'character_context_menu_persona', callback: characterGroupOverlay.handleContextMenuPersona}
|
{id: 'character_context_menu_persona', callback: characterGroupOverlay.handleContextMenuPersona},
|
||||||
|
{id: 'character_context_menu_tag', callback: characterGroupOverlay.handleContextMenuTag}
|
||||||
];
|
];
|
||||||
|
|
||||||
contextMenuItems.forEach(contextMenuItem => document.getElementById(contextMenuItem.id).addEventListener('click', contextMenuItem.callback))
|
contextMenuItems.forEach(contextMenuItem => document.getElementById(contextMenuItem.id).addEventListener('click', contextMenuItem.callback))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends/Removes the bulk tag popup
|
||||||
|
*/
|
||||||
|
class BulkTagPopupHandler {
|
||||||
|
static #getHtml = (characterIds) => {
|
||||||
|
const characterData = JSON.stringify({characterIds: characterIds});
|
||||||
|
return `<div id="bulk_tag_shadow_popup">
|
||||||
|
<div id="bulk_tag_popup">
|
||||||
|
<div id="bulk_tag_popup_holder">
|
||||||
|
<h3 class="m-b-1">Add tags to ${characterIds.length} characters</h3>
|
||||||
|
<br>
|
||||||
|
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
|
||||||
|
<div class="tag_controls">
|
||||||
|
<input id="bulkTagInput" class="text_pole tag_input wide100p margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" />
|
||||||
|
<div class="tags_view menu_button fa-solid fa-tags" title="View all tags" data-i18n="[title]View all tags"></div>
|
||||||
|
</div>
|
||||||
|
<div id="bulkTagList" class="m-t-1 tags"></div>
|
||||||
|
</div>
|
||||||
|
<div id="dialogue_popup_controls" class="m-t-1">
|
||||||
|
<div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
|
||||||
|
<div id="bulk_tag_popup_reset" class="menu_button" data-i18n="Cancel">Remove all</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
static show(characters) {
|
||||||
|
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characters));
|
||||||
|
createTagInput('#bulkTagInput', '#bulkTagList');
|
||||||
|
document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
|
||||||
|
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characters));
|
||||||
|
}
|
||||||
|
|
||||||
|
static hide() {
|
||||||
|
let popupElement = document.querySelector('#bulk_tag_shadow_popup');
|
||||||
|
if (popupElement) {
|
||||||
|
document.body.removeChild(popupElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static resetTags(characterIds) {
|
||||||
|
characterIds.forEach((characterId) => {
|
||||||
|
const key = getTagKeyForCharacter(characterId);
|
||||||
|
if (key) tag_map[key] = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement a SingletonPattern, allowing access to the group overlay instance
|
||||||
|
* from everywhere via (new CharacterGroupOverlay())
|
||||||
|
*
|
||||||
|
* @type BulkEditOverlay
|
||||||
|
*/
|
||||||
|
let bulkEditOverlayInstance = null;
|
||||||
|
|
||||||
class BulkEditOverlay {
|
class BulkEditOverlay {
|
||||||
static containerId = 'rm_print_characters_block';
|
static containerId = 'rm_print_characters_block';
|
||||||
static contextMenuId = 'character_context_menu';
|
static contextMenuId = 'character_context_menu';
|
||||||
@ -191,19 +246,23 @@ class BulkEditOverlay {
|
|||||||
return this.#stateChangeCallbacks;
|
return this.#stateChangeCallbacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {*[]}
|
||||||
|
*/
|
||||||
get selectedCharacters() {
|
get selectedCharacters() {
|
||||||
return this.#selectedCharacters;
|
return this.#selectedCharacters;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (characterGroupOverlayInstance instanceof BulkEditOverlay)
|
if (bulkEditOverlayInstance instanceof BulkEditOverlay)
|
||||||
return characterGroupOverlayInstance
|
return bulkEditOverlayInstance
|
||||||
|
|
||||||
this.container = document.getElementById(BulkEditOverlay.containerId);
|
this.container = document.getElementById(BulkEditOverlay.containerId);
|
||||||
this.container.addEventListener('click', this.handleCancelClick);
|
this.container.addEventListener('click', this.handleCancelClick);
|
||||||
|
|
||||||
eventSource.on(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER, this.handleStateChange);
|
eventSource.on(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER, this.handleStateChange);
|
||||||
characterGroupOverlayInstance = Object.freeze(this);
|
bulkEditOverlayInstance = Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
browseState = () => this.state = CharacterGroupOverlayState.browse;
|
browseState = () => this.state = CharacterGroupOverlayState.browse;
|
||||||
@ -349,6 +408,10 @@ class BulkEditOverlay {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleContextMenuTag = () => {
|
||||||
|
CharacterContextMenu.tag(this.selectedCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback);
|
addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback);
|
||||||
|
|
||||||
selectCharacter = characterId => this.selectedCharacters.push(String(characterId));
|
selectCharacter = characterId => this.selectedCharacters.push(String(characterId));
|
||||||
|
@ -137,8 +137,12 @@ function getTagKey() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTagToMap(tagId) {
|
export function getTagKeyForCharacter(characterId = null) {
|
||||||
const key = getTagKey();
|
return characters[characterId]?.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTagToMap(tagId, characterId = null) {
|
||||||
|
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
@ -152,8 +156,8 @@ function addTagToMap(tagId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTagFromMap(tagId) {
|
function removeTagFromMap(tagId, characterId = null) {
|
||||||
const key = getTagKey();
|
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
@ -197,7 +201,17 @@ function selectTag(event, ui, listSelector) {
|
|||||||
// add tag to the UI and internal map
|
// add tag to the UI and internal map
|
||||||
appendTagToList(listSelector, tag, { removable: true });
|
appendTagToList(listSelector, tag, { removable: true });
|
||||||
appendTagToList(getInlineListSelector(), tag, { removable: false });
|
appendTagToList(getInlineListSelector(), tag, { removable: false });
|
||||||
addTagToMap(tag.id);
|
|
||||||
|
// 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) {
|
||||||
|
characterIds.forEach((characterId) => addTagToMap(tag.id,characterId));
|
||||||
|
} else {
|
||||||
|
addTagToMap(tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
printTagFilters(tag_filter_types.character);
|
printTagFilters(tag_filter_types.character);
|
||||||
printTagFilters(tag_filter_types.group_member);
|
printTagFilters(tag_filter_types.group_member);
|
||||||
@ -383,8 +397,19 @@ function onTagRemoveClick(event) {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const tag = $(this).closest(".tag");
|
const tag = $(this).closest(".tag");
|
||||||
const tagId = tag.attr("id");
|
const tagId = tag.attr("id");
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
tag.remove();
|
tag.remove();
|
||||||
removeTagFromMap(tagId);
|
|
||||||
|
if (characterIds) {
|
||||||
|
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
|
||||||
|
} else {
|
||||||
|
removeTagFromMap(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
||||||
|
|
||||||
printTagFilters(tag_filter_types.character);
|
printTagFilters(tag_filter_types.character);
|
||||||
@ -439,7 +464,7 @@ function applyTagsOnGroupSelect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTagInput(inputSelector, listSelector) {
|
export function createTagInput(inputSelector, listSelector) {
|
||||||
$(inputSelector)
|
$(inputSelector)
|
||||||
.autocomplete({
|
.autocomplete({
|
||||||
source: (i, o) => findTag(i, o, listSelector),
|
source: (i, o) => findTag(i, o, listSelector),
|
||||||
|
@ -1854,6 +1854,7 @@ grammarly-extension {
|
|||||||
|
|
||||||
/* Focus */
|
/* Focus */
|
||||||
|
|
||||||
|
#bulk_tag_popup,
|
||||||
#dialogue_popup {
|
#dialogue_popup {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
@ -1896,6 +1897,7 @@ grammarly-extension {
|
|||||||
width: unset !important;
|
width: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulk_tag_popup_holder,
|
||||||
#dialogue_popup_holder {
|
#dialogue_popup_holder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -1916,6 +1918,7 @@ grammarly-extension {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulk_tag_popup_reset,
|
||||||
#dialogue_popup_ok {
|
#dialogue_popup_ok {
|
||||||
background-color: var(--crimson70a);
|
background-color: var(--crimson70a);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -1926,6 +1929,7 @@ grammarly-extension {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulk_tag_popup_cancel,
|
||||||
#dialogue_popup_cancel {
|
#dialogue_popup_cancel {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user