`;
+ return $(hiddenBlock);
+}
+
function getCharacterBlock(item, id) {
let this_avatar = default_avatar;
if (item.avatar != 'none') {
@@ -1210,9 +1217,9 @@ function getCharacterBlock(item, id) {
// Populate the template
const template = $('#character_template .character_select').clone();
template.attr({ 'chid': id, 'id': `CharID${id}` });
- template.find('img').attr('src', this_avatar);
- template.find('.avatar').attr('title', item.avatar);
- template.find('.ch_name').text(item.name);
+ template.find('img').attr('src', this_avatar).attr('alt', item.name);
+ template.find('.avatar').attr('title', `[Character] ${item.name}`);
+ 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);
}
@@ -1238,9 +1245,8 @@ function getCharacterBlock(item, id) {
}
// Display inline tags
- const tags = getTagsList(item.avatar);
const tagsElement = template.find('.tags');
- tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
+ printTagList(tagsElement, { forEntityOrKey: id });
// Add to the list
return template;
@@ -1252,11 +1258,6 @@ async function printCharacters(fullRefresh = false) {
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
- // Return to main list
- if (isBogusFolderOpen()) {
- entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [] });
- }
-
await delay(1);
}
@@ -1285,19 +1286,28 @@ async function printCharacters(fullRefresh = false) {
if (!data.length) {
$(listId).append(getEmptyBlock());
}
+ let displayCount = 0;
for (const i of data) {
switch (i.type) {
case 'character':
$(listId).append(getCharacterBlock(i.item, i.id));
+ displayCount++;
break;
case 'group':
$(listId).append(getGroupBlock(i.item));
+ displayCount++;
break;
case 'tag':
- $(listId).append(getTagBlock(i.item, entities));
+ $(listId).append(getTagBlock(i.item, i.entities, i.hidden));
break;
}
}
+
+ const hidden = (characters.length + groups.length) - displayCount;
+ if (hidden > 0) {
+ $(listId).append(getHiddenBlock(hidden));
+ }
+
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
},
afterSizeSelectorChange: function (e) {
@@ -1314,15 +1324,7 @@ async function printCharacters(fullRefresh = false) {
favsToHotswap();
}
-/**
- * Indicates whether a user is currently in a bogus folder.
- * @returns {boolean} If currently viewing a folder
- */
-function isBogusFolderOpen() {
- return !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus;
-}
-
-export function getEntitiesList({ doFilter } = {}) {
+export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
function characterToEntity(character, id) {
return { item: character, id, type: 'character' };
}
@@ -1332,36 +1334,53 @@ export function getEntitiesList({ doFilter } = {}) {
}
function tagToEntity(tag) {
- return { item: structuredClone(tag), id: tag.id, type: 'tag' };
+ return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
}
let entities = [
...characters.map((item, index) => characterToEntity(item, index)),
...groups.map(item => groupToEntity(item)),
- ...(power_user.bogus_folders ? tags.map(item => tagToEntity(item)) : []),
+ ...(power_user.bogus_folders ? tags.filter(isBogusFolder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []),
];
+ // We need to do multiple filter runs in a specific order, otherwise different settings might override each other
+ // and screw up tags and search filter, sub lists or similar.
+ // The specific filters are written inside the "filterByTagState" method and its different parameters.
+ // Generally what we do is the following:
+ // 1. First swipe over the list to remove the most obvious things
+ // 2. Build sub entity lists for all folders, filtering them similarly to the second swipe
+ // 3. We do the last run, where global filters are applied, and the search filters last
+
+ // First run filters, that will hide what should never be displayed
if (doFilter) {
+ entities = filterByTagState(entities);
+ }
+
+ // Run over all entities between first and second filter to save some states
+ for (const entity of entities) {
+ // For folders, we remember the sub entities so they can be displayed later, even if they might be filtered
+ // Those sub entities should be filtered and have the search filters applied too
+ if (entity.type === 'tag') {
+ let subEntities = filterByTagState(entities, { subForEntity: entity, filterHidden: false });
+ const subCount = subEntities.length;
+ subEntities = filterByTagState(entities, { subForEntity: entity });
+ if (doFilter) {
+ subEntities = entitiesFilter.applyFilters(subEntities);
+ }
+ entity.entities = subEntities;
+ entity.hidden = subCount - subEntities.length;
+ }
+ }
+
+ // Second run filters, hiding whatever should be filtered later
+ if (doFilter) {
+ entities = filterByTagState(entities, { globalDisplayFilters: true });
entities = entitiesFilter.applyFilters(entities);
}
- if (isBogusFolderOpen()) {
- // Get tags of entities within the bogus folder
- const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
- entities = entities.filter(x => x.type !== 'tag');
- const otherTags = tags.filter(x => !filterData.selected.includes(x.id));
- const bogusTags = [];
- for (const entity of entities) {
- for (const tag of otherTags) {
- if (!bogusTags.includes(tag) && entitiesFilter.isElementTagged(entity, tag.id)) {
- bogusTags.push(tag);
- }
- }
- }
- entities.push(...bogusTags.map(item => tagToEntity(item)));
+ if (doSort) {
+ sortEntitiesList(entities);
}
-
- sortEntitiesList(entities);
return entities;
}
@@ -5261,6 +5280,51 @@ function getThumbnailUrl(type, file) {
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
}
+function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) {
+ if (empty) {
+ block.empty();
+ }
+
+ for (const entity of entities) {
+ const id = entity.id;
+
+ // Populate the template
+ const avatarTemplate = $(`#${templateId} .avatar`).clone();
+
+ let this_avatar = default_avatar;
+ if (entity.item.avatar !== undefined && entity.item.avatar != 'none') {
+ this_avatar = getThumbnailUrl('avatar', entity.item.avatar);
+ }
+
+ avatarTemplate.attr('data-type', entity.type);
+ avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` });
+ avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name);
+ avatarTemplate.attr('title', `[Character] ${entity.item.name}`);
+ if (highlightFavs) {
+ avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true');
+ avatarTemplate.find('.ch_fav').val(entity.item.fav);
+ }
+
+ // If this is a group, we need to hack slightly. We still want to keep most of the css classes and layout, but use a group avatar instead.
+ if (entity.type === 'group') {
+ const grpTemplate = getGroupAvatar(entity.item);
+
+ avatarTemplate.addClass(grpTemplate.attr('class'));
+ avatarTemplate.empty();
+ avatarTemplate.append(grpTemplate.children());
+ avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
+ }
+
+ if (selectable) {
+ avatarTemplate.addClass('selectable');
+ avatarTemplate.toggleClass('character_select', entity.type === 'character');
+ avatarTemplate.toggleClass('group_select', entity.type === 'group');
+ }
+
+ block.append(avatarTemplate);
+ }
+}
+
async function getChat() {
//console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
try {
@@ -5287,9 +5351,12 @@ async function getChat() {
await getChatResult();
eventSource.emit('chatLoaded', { detail: { id: this_chid, character: characters[this_chid] } });
+ // Focus on the textarea if not already focused on a visible text input
setTimeout(function () {
- $('#send_textarea').click();
- $('#send_textarea').focus();
+ if ($(document.activeElement).is('input:visible, textarea:visible')) {
+ return;
+ }
+ $('#send_textarea').trigger('click').trigger('focus');
}, 200);
} catch (error) {
await getChatResult();
@@ -8271,25 +8338,8 @@ jQuery(async function () {
$(document).on('click', '.bogus_folder_select', function () {
const tagId = $(this).attr('tagid');
- console.log('Bogus folder clicked', tagId);
-
- const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
-
- if (!Array.isArray(filterData.selected)) {
- filterData.selected = [];
- filterData.excluded = [];
- filterData.bogus = false;
- }
-
- if (tagId === 'back') {
- filterData.selected.pop();
- filterData.bogus = filterData.selected.length > 0;
- } else {
- filterData.selected.push(tagId);
- filterData.bogus = true;
- }
-
- entitiesFilter.setFilterData(FILTER_TYPES.TAG, filterData);
+ console.debug('Bogus folder clicked', tagId);
+ chooseBogusFolder($(this), tagId);
});
$(document).on('input', '.edit_textarea', function () {
diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js
index d12e1599e..e1db7f77b 100644
--- a/public/scripts/BulkEditOverlay.js
+++ b/public/scripts/BulkEditOverlay.js
@@ -15,7 +15,7 @@ import {
import { favsToHotswap } from './RossAscends-mods.js';
import { hideLoader, showLoader } from './loader.js';
import { convertCharacterToPersona } from './personas.js';
-import { createTagInput, getTagKeyForCharacter, tag_map } from './tags.js';
+import { createTagInput, getTagKeyForEntity, tag_map } from './tags.js';
// Utility object for popup messages.
const popupMessage = {
@@ -243,7 +243,7 @@ class BulkTagPopupHandler {
*/
static resetTags(characterIds) {
characterIds.forEach((characterId) => {
- const key = getTagKeyForCharacter(characterId);
+ const key = getTagKeyForEntity(characterId);
if (key) tag_map[key] = [];
});
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index 077de3050..8589c8ae2 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -11,7 +11,7 @@ import {
setActiveGroup,
setActiveCharacter,
getEntitiesList,
- getThumbnailUrl,
+ buildAvatarList,
selectCharacterById,
eventSource,
menu_type,
@@ -26,7 +26,8 @@ import {
} from './power-user.js';
import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
-import { selected_group, is_group_generating, getGroupAvatar, groups, openGroupById } from './group-chats.js';
+import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
+import { getTagKeyForEntity } from './tags.js';
import {
SECRET_KEYS,
secret_state,
@@ -247,13 +248,14 @@ export function RA_CountCharTokens() {
async function RA_autoloadchat() {
if (document.querySelector('#rm_print_characters_block .character_select') !== null) {
// active character is the name, we should look it up in the character list and get the id
- let active_character_id = Object.keys(characters).find(key => characters[key].avatar === active_character);
-
- if (active_character_id !== null) {
- await selectCharacterById(String(active_character_id));
+ if (active_character !== null && active_character !== undefined) {
+ const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
+ if (active_character_id !== null) {
+ await selectCharacterById(String(active_character_id));
+ }
}
- if (active_group != null) {
+ if (active_group !== null && active_group !== undefined) {
await openGroupById(String(active_group));
}
@@ -264,84 +266,16 @@ async function RA_autoloadchat() {
export async function favsToHotswap() {
const entities = getEntitiesList({ doFilter: false });
const container = $('#right-nav-panel .hotswap');
- const template = $('#hotswap_template .hotswapAvatar');
- const DEFAULT_COUNT = 6;
- const WIDTH_PER_ITEM = 60; // 50px + 5px gap + 5px padding
- const containerWidth = container.outerWidth();
- const maxCount = containerWidth > 0 ? Math.floor(containerWidth / WIDTH_PER_ITEM) : DEFAULT_COUNT;
- let count = 0;
- const promises = [];
- const newContainer = container.clone();
- newContainer.empty();
+ const favs = entities.filter(x => x.item.fav || x.item.fav == 'true');
- for (const entity of entities) {
- if (count >= maxCount) {
- break;
- }
-
- const isFavorite = entity.item.fav || entity.item.fav == 'true';
-
- if (!isFavorite) {
- continue;
- }
-
- const isCharacter = entity.type === 'character';
- const isGroup = entity.type === 'group';
-
- const grid = isGroup ? entity.id : '';
- const chid = isCharacter ? entity.id : '';
-
- let slot = template.clone();
- slot.toggleClass('character_select', isCharacter);
- slot.toggleClass('group_select', isGroup);
- slot.attr('grid', isGroup ? grid : '');
- slot.attr('chid', isCharacter ? chid : '');
- slot.data('id', isGroup ? grid : chid);
-
- if (isGroup) {
- const group = groups.find(x => x.id === grid);
- const avatar = getGroupAvatar(group);
- $(slot).find('img').replaceWith(avatar);
- $(slot).attr('title', group.name);
- }
-
- if (isCharacter) {
- const imgLoadPromise = new Promise((resolve) => {
- const avatarUrl = getThumbnailUrl('avatar', entity.item.avatar);
- $(slot).find('img').attr('src', avatarUrl).on('load', resolve);
- $(slot).attr('title', entity.item.avatar);
- });
-
- // if the image doesn't load in 500ms, resolve the promise anyway
- promises.push(Promise.race([imgLoadPromise, delay(500)]));
- }
-
- $(slot).css('cursor', 'pointer');
- newContainer.append(slot);
- count++;
- }
-
- // don't fill leftover spaces with avatar placeholders
- // just evenly space the selected avatars instead
- /*
- if (count < maxCount) { //if any space is left over
- let leftOverSlots = maxCount - count;
- for (let i = 1; i <= leftOverSlots; i++) {
- newContainer.append(template.clone());
- }
- }
- */
-
- await Promise.allSettled(promises);
//helpful instruction message if no characters are favorited
- if (count === 0) {
- container.html(' Favorite characters to add them to HotSwaps');
- }
- //otherwise replace with fav'd characters
- if (count > 0) {
- container.replaceWith(newContainer);
+ if (favs.length == 0) {
+ container.html('Favorite characters to add them to HotSwaps');
+ return;
}
+
+ buildAvatarList(container, favs, { selectable: true, highlightFavs: false });
}
//changes input bar and send button display depending on connection status
@@ -873,14 +807,14 @@ export function initRossMods() {
// when a char is selected from the list, save their name as the auto-load character for next page load
$(document).on('click', '.character_select', function () {
- const characterId = $(this).find('.avatar').attr('title') || $(this).attr('title');
+ const characterId = $(this).attr('chid') || $(this).data('id');
setActiveCharacter(characterId);
setActiveGroup(null);
saveSettingsDebounced();
});
$(document).on('click', '.group_select', function () {
- const groupId = $(this).data('id') || $(this).attr('grid');
+ const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id');
setActiveCharacter(null);
setActiveGroup(groupId);
saveSettingsDebounced();
diff --git a/public/scripts/filters.js b/public/scripts/filters.js
index 4a0efc3fc..d880cbf37 100644
--- a/public/scripts/filters.js
+++ b/public/scripts/filters.js
@@ -8,12 +8,37 @@ import { tag_map } from './tags.js';
export const FILTER_TYPES = {
SEARCH: 'search',
TAG: 'tag',
+ FOLDER: 'folder',
FAV: 'fav',
GROUP: 'group',
WORLD_INFO_SEARCH: 'world_info_search',
PERSONA_SEARCH: 'persona_search',
};
+/**
+ * The filter states.
+ * @type {Object.}
+ */
+export const FILTER_STATES = {
+ SELECTED: { key: 'SELECTED', class: 'selected' },
+ EXCLUDED: { key: 'EXCLUDED', class: 'excluded' },
+ UNDEFINED: { key: 'UNDEFINED', class: 'undefined' },
+};
+
+/**
+ * Robust check if one state equals the other. It does not care whether it's the state key or the state value object.
+ * @param {Object} a First state
+ * @param {Object} b Second state
+ */
+export function isFilterState(a, b) {
+ const states = Object.keys(FILTER_STATES);
+
+ const aKey = states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a);
+ const bKey = states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b);
+
+ return aKey === bKey;
+}
+
/**
* Helper class for filtering data.
* @example
@@ -36,8 +61,9 @@ export class FilterHelper {
*/
filterFunctions = {
[FILTER_TYPES.SEARCH]: this.searchFilter.bind(this),
- [FILTER_TYPES.GROUP]: this.groupFilter.bind(this),
[FILTER_TYPES.FAV]: this.favFilter.bind(this),
+ [FILTER_TYPES.GROUP]: this.groupFilter.bind(this),
+ [FILTER_TYPES.FOLDER]: this.folderFilter.bind(this),
[FILTER_TYPES.TAG]: this.tagFilter.bind(this),
[FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.bind(this),
[FILTER_TYPES.PERSONA_SEARCH]: this.personaSearchFilter.bind(this),
@@ -49,8 +75,9 @@ export class FilterHelper {
*/
filterData = {
[FILTER_TYPES.SEARCH]: '',
- [FILTER_TYPES.GROUP]: false,
[FILTER_TYPES.FAV]: false,
+ [FILTER_TYPES.GROUP]: false,
+ [FILTER_TYPES.FOLDER]: false,
[FILTER_TYPES.TAG]: { excluded: [], selected: [] },
[FILTER_TYPES.WORLD_INFO_SEARCH]: '',
[FILTER_TYPES.PERSONA_SEARCH]: '',
@@ -116,6 +143,7 @@ export class FilterHelper {
}
const getIsTagged = (entity) => {
+ const isTag = entity.type === 'tag';
const tagFlags = selected.map(tagId => this.isElementTagged(entity, tagId));
const trueFlags = tagFlags.filter(x => x);
const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0;
@@ -123,7 +151,9 @@ export class FilterHelper {
const excludedTagFlags = excluded.map(tagId => this.isElementTagged(entity, tagId));
const isExcluded = excludedTagFlags.includes(true);
- if (isExcluded) {
+ if (isTag) {
+ return true;
+ } else if (isExcluded) {
return false;
} else if (selected.length > 0 && !isTagged) {
return false;
@@ -141,11 +171,10 @@ export class FilterHelper {
* @returns {any[]} The filtered data.
*/
favFilter(data) {
- if (!this.filterData[FILTER_TYPES.FAV]) {
- return data;
- }
+ const state = this.filterData[FILTER_TYPES.FAV];
+ const isFav = entity => entity.item.fav || entity.item.fav == 'true';
- return data.filter(entity => entity.item.fav || entity.item.fav == 'true');
+ return this.filterDataByState(data, state, isFav, { includeFolders: true });
}
/**
@@ -154,11 +183,35 @@ export class FilterHelper {
* @returns {any[]} The filtered data.
*/
groupFilter(data) {
- if (!this.filterData[FILTER_TYPES.GROUP]) {
- return data;
+ const state = this.filterData[FILTER_TYPES.GROUP];
+ const isGroup = entity => entity.type === 'group';
+
+ return this.filterDataByState(data, state, isGroup, { includeFolders: true });
+ }
+
+ /**
+ * Applies a "folder" filter to the data.
+ * @param {any[]} data The data to filter.
+ * @returns {any[]} The filtered data.
+ */
+ folderFilter(data) {
+ const state = this.filterData[FILTER_TYPES.FOLDER];
+ // Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
+ // while a negative state should then filter out all folders.
+ const isFolder = entity => isFilterState(state, FILTER_STATES.SELECTED) ? true : entity.type === 'tag';
+
+ return this.filterDataByState(data, state, isFolder);
+ }
+
+ filterDataByState(data, state, filterFunc, { includeFolders } = {}) {
+ if (isFilterState(state, FILTER_STATES.SELECTED)) {
+ return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag'));
+ }
+ if (isFilterState(state, FILTER_STATES.EXCLUDED)) {
+ return data.filter(entity => !filterFunc(entity) || (includeFolders && entity.type == 'tag'));
}
- return data.filter(entity => entity.type === 'group');
+ return data;
}
/**
diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js
index 52ec5078c..57da890ee 100644
--- a/public/scripts/group-chats.js
+++ b/public/scripts/group-chats.js
@@ -69,7 +69,7 @@ import {
loadItemizedPrompts,
animation_duration,
} from '../script.js';
-import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
+import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
export {
@@ -530,18 +530,33 @@ async function getGroups() {
}
export function getGroupBlock(group) {
+ let count = 0;
+ let namesList = [];
+
+ // Build inline name list
+ if (Array.isArray(group.members) && group.members.length) {
+ for (const member of group.members) {
+ const character = characters.find(x => x.avatar === member || x.name === member);
+ if (character) {
+ namesList.push(character.name);
+ count++;
+ }
+ }
+ }
+
const template = $('#group_list_template .group_select').clone();
template.data('id', group.id);
template.attr('grid', group.id);
- template.find('.ch_name').text(group.name);
+ template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`);
template.find('.group_fav_icon').css('display', 'none');
template.addClass(group.fav ? 'is_fav' : '');
template.find('.ch_fav').val(group.fav);
+ template.find('.group_select_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
+ template.find('.group_select_block_list').text(namesList.join(', '));
// Display inline tags
- const tags = getTagsList(group.id);
const tagsElement = template.find('.tags');
- tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
+ printTagList(tagsElement, { forEntityOrKey: group.id });
const avatar = getGroupAvatar(group);
if (avatar) {
@@ -559,6 +574,8 @@ function updateGroupAvatar(group) {
$(this).find('.avatar').replaceWith(getGroupAvatar(group));
}
});
+
+ favsToHotswap();
}
// check if isDataURLor if it's a valid local file url
@@ -576,7 +593,7 @@ function getGroupAvatar(group) {
}
// if isDataURL or if it's a valid local file url
if (isValidImageUrl(group.avatar_url)) {
- return $(``);
+ return $(``);
}
const memberAvatars = [];
@@ -602,6 +619,7 @@ function getGroupAvatar(group) {
groupAvatar.find(`.img_${i + 1}`).attr('src', memberAvatars[i]);
}
+ groupAvatar.attr('title', `[Group] ${group.name}`);
return groupAvatar;
}
@@ -613,6 +631,7 @@ function getGroupAvatar(group) {
// default avatar
const groupAvatar = $('#group_avatars_template .collage_1').clone();
groupAvatar.find('.img_1').attr('src', group.avatar_url || system_avatar);
+ groupAvatar.attr('title', `[Group] ${group.name}`);
return groupAvatar;
}
@@ -1176,9 +1195,8 @@ function getGroupCharacterBlock(character) {
template.toggleClass('disabled', isGroupMemberDisabled(character.avatar));
// Display inline tags
- const tags = getTagsList(character.avatar);
const tagsElement = template.find('.tags');
- tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
+ printTagList(tagsElement, { forEntityOrKey: characters.indexOf(character) });
if (!openGroupId) {
template.find('[data-action="speak"]').hide();
@@ -1254,6 +1272,9 @@ function select_group_chats(groupId, skipAnimation) {
selectRightMenuWithAnimation('rm_group_chats_block');
}
+ // render tags
+ printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
+
// render characters list
printGroupCandidates();
printGroupMembers();
@@ -1751,7 +1772,7 @@ function doCurMemberListPopout() {
jQuery(() => {
$(document).on('click', '.group_select', function () {
- const groupId = $(this).data('id');
+ const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id');
openGroupById(groupId);
});
$('#rm_group_filter').on('input', filterGroupMembers);
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index 4762443f7..a9d64c1a8 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -235,6 +235,7 @@ let power_user = {
encode_tags: false,
servers: [],
bogus_folders: false,
+ show_tag_filters: false,
aux_field: 'character_version',
restore_user_input: true,
reduced_motion: false,
diff --git a/public/scripts/tags.js b/public/scripts/tags.js
index f6a756278..2615d87b3 100644
--- a/public/scripts/tags.js
+++ b/public/scripts/tags.js
@@ -7,23 +7,35 @@ import {
getCharacters,
entitiesFilter,
printCharacters,
+ buildAvatarList,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
-import { FILTER_TYPES, FilterHelper } from './filters.js';
+import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
-import { download, onlyUnique, parseJsonFile, uuidv4 } from './utils.js';
+import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js';
+import { power_user } from './power-user.js';
export {
+ TAG_FOLDER_TYPES,
+ TAG_FOLDER_DEFAULT_TYPE,
tags,
tag_map,
+ filterByTagState,
+ isBogusFolder,
+ isBogusFolderOpen,
+ chooseBogusFolder,
+ getTagBlock,
loadTagsSettings,
printTagFilters,
getTagsList,
+ printTagList,
appendTagToList,
createTagMapFromList,
renameTagKey,
importTags,
+ sortTags,
+ compareTagsForSort,
};
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@@ -33,16 +45,24 @@ function getFilterHelper(listSelector) {
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
}
+const redrawCharsAndFiltersDebounced = debounce(() => {
+ printCharacters(false);
+ printTagFilters(tag_filter_types.character);
+ printTagFilters(tag_filter_types.group_member);
+}, 100);
+
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' },
+ FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, 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' },
+ FOLDER: { id: 4, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
+ UNFILTER: { id: 5, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' },
};
const InListActionable = {
@@ -57,19 +77,170 @@ const DEFAULT_TAGS = [
{ id: uuidv4(), name: 'AliChat', create_date: Date.now() },
];
+const TAG_FOLDER_TYPES = {
+ OPEN: { icon: '✔', class: 'folder_open', fa_icon: 'fa-folder-open', tooltip: 'Open Folder (Show all characters even if not selected)', color: 'green', size: '1' },
+ CLOSED: { icon: '👁', class: 'folder_closed', fa_icon: 'fa-eye-slash', tooltip: 'Closed Folder (Hide all characters unless selected)', color: 'lightgoldenrodyellow', size: '0.7' },
+ NONE: { icon: '✕', class: 'no_folder', tooltip: 'No Folder', color: 'red', size: '1' },
+};
+const TAG_FOLDER_DEFAULT_TYPE = 'NONE';
+
+
let tags = [];
let tag_map = {};
+/**
+ * Applies the basic filter for the current state of the tags and their selection on an entity list.
+ * @param {Array
+ Drag the handle to reorder.
+ ${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder. ' : '')}
Click on the tag name to edit it.
Click on color box to assign new color.