Add bulk tagging

This commit is contained in:
artisticMink 2023-11-04 19:33:15 +01:00
parent 545d933e15
commit c3ff146dd2
6 changed files with 142 additions and 26 deletions

View File

@ -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;
}

View File

@ -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;
} }

View File

@ -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>

View File

@ -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));

View File

@ -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),

View File

@ -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;
} }