Tag Folders: hidden/closed folders

- Implement folder types: Open, Closed, None
- Closed folders hide characters from most places
- "character(s)" singular wording on entity list
- small refactoring for that code
This commit is contained in:
Wolfsblvt 2024-03-06 00:28:14 +01:00
parent c0e112d195
commit e578d3dbb6
6 changed files with 173 additions and 80 deletions

View File

@ -158,7 +158,7 @@
border: 1px solid red;
}
.tag.excluded:after {
.tag.excluded::after {
position: absolute;
top: 0;
bottom: 0;
@ -177,22 +177,21 @@
}
.tag_as_folder {
filter: brightness(25%) saturate(0.25);
}
.tag_as_folder.yes_folder {
filter: brightness(75%) saturate(0.6);
}
.tag_as_folder:hover {
filter: brightness(150%) saturate(0.6);
filter: brightness(150%) saturate(0.6) !important;
}
.tag_as_folder.yes_folder:after {
.tag_as_folder.no_folder {
filter: brightness(25%) saturate(0.25);
}
.tag_as_folder .tag_folder_indicator {
position: absolute;
top: calc(var(--mainFontSize) * -0.5);
right: calc(var(--mainFontSize) * -0.5);
content: "\2714";
font-size: calc(var(--mainFontSize) * 1);
color: green;
line-height: calc(var(--mainFontSize) * 1.3);
text-align: center;
text-shadow: 1px 1px 0px black,
@ -202,23 +201,7 @@
opacity: 1;
}
.tag_as_folder.no_folder:after {
position: absolute;
top: calc(var(--mainFontSize) * -0.5);
right: calc(var(--mainFontSize) * -0.5);
content: "\2715";
font-size: calc(var(--mainFontSize) * 1);
color: red;
line-height: calc(var(--mainFontSize) * 1.3);
text-align: center;
text-shadow: 1px 1px 0px black,
-1px -1px 0px black,
-1px 1px 0px black,
1px -1px 0px black;
opacity: 1;
}
.tag.indicator:after {
.tag.indicator::after {
position: absolute;
top: calc(var(--mainFontSize) * -0.5);
right: -2px;
@ -239,7 +222,7 @@
margin-left: calc(var(--mainFontSize) * 2);
}
.rm_tag_bogus_drilldown .tag:not(:first-child):before {
.rm_tag_bogus_drilldown .tag:not(:first-child)::before {
position: absolute;
left: calc(var(--mainFontSize) * -2);
top: -1px;

View File

@ -4544,7 +4544,9 @@
<div id="tag_view_template" class="template_element">
<div class="tag_view_item">
<div class="drag-handle" data-i18n="[title]Drag to reorder tag"></div>
<div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder"></div>
<div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder">
<span class="tag_folder_indicator"></span>
</div>
<div class="tagColorPickerHolder"></div>
<div class="tagColorPicker2Holder"></div>
<div class="tag_view_name" contenteditable="true"></div>
@ -5011,7 +5013,7 @@
<div class="ch_name"></div>
<small class="group_select_unit" data="characters">group of</small>
<small class="character_version group_select_counter">5</small>
<small class="group_select_unit" data="characters">characters</small>
<small class="group_select_unit character_unit_name" data="characters">characters</small>
</div>
<i class='group_fav_icon fa-solid fa-star'></i>
<input class="ch_fav" value="" hidden />
@ -5023,13 +5025,13 @@
<div id="bogus_folder_template" class="template_element">
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart">
<div class="avatar flex alignitemscenter textAlignCenter">
<i class="bogus_folder_icon fa-solid fa-folder-open fa-xl"></i>
<i class="bogus_folder_icon fa-solid fa-xl"></i>
</div>
<div class="flex-container wide100pLess70px character_select_container">
<div class="wide100p character_name_block">
<span class="ch_name"></span>
<small class="character_version bogus_folder_counter"></small>
<small class="bogus_folder_unit" data="characters">characters</small>
<small class="bogus_folder_unit character_unit_name" data="characters">characters</small>
</div>
<div class="bogus_folder_avatars_block avatars_inline tags tags_inline"></div>
</div>

View File

@ -153,8 +153,10 @@ import {
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js';
import {
TAG_FOLDER_DEFAULT_TYPE,
tag_map,
tags,
filterByTagState,
loadTagsSettings,
printTagFilters,
getTagsList,
@ -164,6 +166,7 @@ import {
importTags,
tag_filter_types,
compareTagsForSort,
TAG_FOLDER_TYPES,
} from './scripts/tags.js';
import {
SECRET_KEYS,
@ -273,6 +276,7 @@ export {
isOdd,
countOccurrences,
chooseBogusFolder,
isBogusFolder,
};
showLoader();
@ -1158,25 +1162,24 @@ export async function selectCharacterById(id) {
}
function getTagBlock(item, entities) {
let count = 0;
let subEntities = [];
let count = entities.length;
for (const entity of entities) {
if (entitiesFilter.isElementTagged(entity, item.id)) {
count++;
subEntities.push(entity);
}
}
const tagFolder = TAG_FOLDER_TYPES[item.folder_type];
const template = $('#bogus_folder_template .bogus_folder_select').clone();
template.addClass(tagFolder.class);
template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` });
template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 }).attr('title', `[Folder] ${item.name}`);
template.find('.ch_name').text(item.name).attr('title', `[Folder] ${item.name}`);;
template.find('.ch_name').text(item.name).attr('title', `[Folder] ${item.name}`);
template.find('.bogus_folder_counter').text(count);
template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);
if (count == 1) {
template.find('.character_unit_name').text('character');
}
// Fill inline character images
const inlineAvatars = template.find('.bogus_folder_avatars_block');
for (const entitiy of subEntities) {
for (const entitiy of entities) {
const id = entitiy.id;
// Populate the template
@ -1314,7 +1317,7 @@ async function printCharacters(fullRefresh = false) {
$(listId).append(getGroupBlock(i.item));
break;
case 'tag':
$(listId).append(getTagBlock(i.item, entities));
$(listId).append(getTagBlock(i.item, i.entities ?? entities));
break;
}
}
@ -1334,6 +1337,14 @@ async function printCharacters(fullRefresh = false) {
favsToHotswap();
}
/**
* Indicates whether a given tag is defined as a folder. Meaning it's neither undefined nor 'NONE'.
* @returns {boolean} If it's a tag folder
*/
function isBogusFolder(tag) {
return tag?.folder_type !== undefined && tag.folder_type !== TAG_FOLDER_DEFAULT_TYPE;
}
/**
* Indicates whether a user is currently in a bogus folder.
* @returns {boolean} If currently viewing a folder
@ -1341,7 +1352,7 @@ async function printCharacters(fullRefresh = false) {
function isBogusFolderOpen() {
const anyIsFolder = entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.selected
.map(tagId => tags.find(x => x.id === tagId))
.some(x => !!x.is_folder);
.some(isBogusFolder);
return !!anyIsFolder;
}
@ -1356,33 +1367,33 @@ 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.filter(x => x.is_folder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []),
...(power_user.bogus_folders ? tags.filter(isBogusFolder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []),
];
// First run filters, that will hide what should never be displayed
if (doFilter) {
entities = entitiesFilter.applyFilters(entities);
entities = filterByTagState(entities);
}
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
entities = entities.filter(entity => {
// 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
if (entity.type === 'tag') {
// Remove filtered tags/bogus folders
if (filterData.selected.includes(entity.id) || filterData.excluded.includes(entity.id)) {
return false;
}
// Check if tag is used in any other entities, removing 0 count folders
return entities.some(e => e.type !== 'tag' && entitiesFilter.isElementTagged(e, entity.id));
entity.entities = filterByTagState(entities, { subForEntity: entity });
}
return true;
});
}
// Second run filters, hiding whatever should be filtered later
if (doFilter) {
entities = filterByTagState(entities, { globalDisplayFilters: true });
}
sortEntitiesList(entities);
return entities;
@ -8089,22 +8100,12 @@ function doTogglePanels() {
}
function chooseBogusFolder(source, tagId, remove = false) {
// Take the filter as the base on what bogus is currently selected
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
if (!Array.isArray(filterData.selected)) {
filterData.selected = [];
filterData.excluded = [];
}
const filteredFolders = filterData.selected
.map(tagId => tags.find(x => x.id === tagId))
.filter(x => !!x.is_folder);
// If we are here via the 'back' action, we implicitly take the last filtered folder as one to remove
const isBack = tagId === 'back';
if (isBack) {
tagId = filteredFolders?.[filteredFolders.length - 1].id;
const drilldown = $(source).closest('#rm_characters_block').find('.rm_tag_bogus_drilldown');
const lastTag = drilldown.find('.tag:last').last();
tagId = lastTag.attr('id');
remove = true;
}
@ -8293,7 +8294,7 @@ jQuery(async function () {
$(document).on('click', '.bogus_folder_select', function () {
const tagId = $(this).attr('tagid');
console.log('Bogus folder clicked', tagId);
console.debug('Bogus folder clicked', tagId);
chooseBogusFolder($(this), tagId);
});

View File

@ -544,6 +544,9 @@ export function getGroupBlock(group) {
template.find('.ch_fav').val(group.fav);
template.find('.group_select_counter').text(count);
template.find('.group_select_block_list').append(namesList.join(''));
if (count == 1) {
template.find('.character_unit_name').text('character');
}
// Display inline tags
const tags = getTagsList(group.id);

View File

@ -8,6 +8,7 @@ import {
entitiesFilter,
printCharacters,
chooseBogusFolder,
isBogusFolder,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FilterHelper } from './filters.js';
@ -17,8 +18,11 @@ import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from '.
import { power_user } from './power-user.js';
export {
TAG_FOLDER_TYPES,
TAG_FOLDER_DEFAULT_TYPE,
tags,
tag_map,
filterByTagState,
loadTagsSettings,
printTagFilters,
getTagsList,
@ -61,9 +65,85 @@ 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 {*} entities List of entities for display, consisting of tags, characters and groups.
*/
function filterByTagState(entities, { globalDisplayFilters = false, subForEntity = undefined } = {}) {
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
entities = entities.filter(entity => {
if (entity.type === 'tag') {
// Remove folders that are already filtered on
if (filterData.selected.includes(entity.id) || filterData.excluded.includes(entity.id)) {
return false;
}
}
return true;
});
if (globalDisplayFilters) {
// Prepare some data for caching and performance
const closedFolders = entities.filter(x => x.type === 'tag' && TAG_FOLDER_TYPES[x.item.folder_type] === TAG_FOLDER_TYPES.CLOSED);
entities = entities.filter(entity => {
// Hide entities that are in a closed folder, unless that one is opened
if (entity.type !== 'tag' && closedFolders.some(f => entitiesFilter.isElementTagged(entity, f.id) && !filterData.selected.includes(f.id))) {
return false;
}
// Hide folders that have 0 visible sub entities after the first filtering round
if (entity.type === 'tag') {
return entity.entities.length > 0;
}
return true;
});
}
if (subForEntity !== undefined && subForEntity.type === 'tag') {
entities = filterTagSubEntities(subForEntity.item, entities);
}
return entities;
}
function filterTagSubEntities(tag, entities) {
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
const closedFolders = entities.filter(x => x.type === 'tag' && TAG_FOLDER_TYPES[x.item.folder_type] === TAG_FOLDER_TYPES.CLOSED);
entities = entities.filter(sub => {
// Filter out all tags and and all who isn't tagged for this item
if (sub.type === 'tag' || !entitiesFilter.isElementTagged(sub, tag.id)) {
return false;
}
// Hide entities that are in a closed folder, unless the closed folder is opened or we display a closed folder
if (sub.type !== 'tag' && TAG_FOLDER_TYPES[tag.folder_type] !== TAG_FOLDER_TYPES.CLOSED && closedFolders.some(f => entitiesFilter.isElementTagged(sub, f.id) && !filterData.selected.includes(f.id))) {
return false;
}
return true;
});
return entities;
}
/**
* Applies the favorite filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
@ -282,7 +362,7 @@ function createNewTag(tagName) {
const tag = {
id: uuidv4(),
name: tagName,
is_folder: false,
folder_type: TAG_FOLDER_DEFAULT_TYPE,
sort_order: tags.length,
color: '',
color2: '',
@ -372,7 +452,7 @@ function onTagFilterClick(listElement) {
}
// Update bogus folder if applicable
if (existingTag?.is_folder) {
if (isBogusFolder(existingTag)) {
// Update bogus drilldown
if ($(this).hasClass('selected')) {
appendTagToList('.rm_tag_controls .rm_tag_bogus_drilldown', existingTag, { removable: true, selectable: false, isGeneralList: false });
@ -760,7 +840,6 @@ function appendViewTagToList(list, tag, everything) {
template.find('.tag_as_folder').hide();
}
template.find('.tag_as_folder').addClass(tag.is_folder == true ? 'yes_folder' : 'no_folder');
template.find('.tagColorPickerHolder').html(
`<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`,
);
@ -786,6 +865,8 @@ function appendViewTagToList(list, tag, everything) {
});
}, 100);
updateDrawTagFolder(template, tag);
// @ts-ignore
$(colorPickerId).color = tag.color;
// @ts-ignore
@ -793,12 +874,16 @@ function appendViewTagToList(list, tag, everything) {
}
function onTagAsFolderClick() {
const id = $(this).closest('.tag_view_item').attr('id');
const element = $(this).closest('.tag_view_item');
const id = element.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');
// Cycle through folder types
const types = Object.keys(TAG_FOLDER_TYPES);
let currentTypeIndex = types.indexOf(tag.folder_type);
tag.folder_type = types[(currentTypeIndex + 1) % types.length];
updateDrawTagFolder(element, tag);
// If folder display has changed, we have to redraw the character list, otherwise this folders state would not change
printCharacters(true);
@ -806,6 +891,23 @@ function onTagAsFolderClick() {
}
function updateDrawTagFolder(element, tag) {
const tagFolder = TAG_FOLDER_TYPES[tag.folder_type] || TAG_FOLDER_TYPES[TAG_FOLDER_DEFAULT_TYPE];
const folderElement = element.find('.tag_as_folder');
// Update css class and remove all others
Object.keys(TAG_FOLDER_TYPES).forEach(x => {
folderElement.toggleClass(TAG_FOLDER_TYPES[x].class, TAG_FOLDER_TYPES[x] === tagFolder);
});
// Draw/update css attributes for this class
folderElement.attr('title', tagFolder.tooltip);
const indicator = folderElement.find('.tag_folder_indicator');
indicator.text(tagFolder.icon);
indicator.css('color', tagFolder.color);
indicator.css('font-size', `calc(var(--mainFontSize) * ${tagFolder.size})`);
}
function onTagDeleteClick() {
if (!confirm('Are you sure?')) {
return;
@ -859,7 +961,6 @@ function onTagColorize2(evt) {
}
function onTagListHintClick() {
console.debug($(this));
$(this).toggleClass('selected');
$(this).siblings('.tag:not(.actionable)').toggle(100);
$(this).siblings('.innerActionable').toggleClass('hidden');
@ -867,7 +968,7 @@ function onTagListHintClick() {
power_user.show_tag_filters = $(this).hasClass('selected');
saveSettingsDebounced();
console.log('show_tag_filters', power_user.show_tag_filters);
console.debug('show_tag_filters', power_user.show_tag_filters);
}
jQuery(() => {

View File

@ -966,7 +966,10 @@ hr {
flex-wrap: wrap;
overflow: hidden;
height: calc(var(--avatar-base-height) * var(--inline-avatar-factor) + 2 * var(--avatar-base-border-radius));
/* margin-top: calc(var(--avatar-base-height) * var(--inline-avatar-factor) * 0.2); */
}
.bogus_folder_select:not(.folder_closed) .bogus_folder_avatars_block {
opacity: 1 !important;
}
.bogus_folder_avatars_block .avatar {