From 25b528ee4fcf87991db79c29a387567d4de725d0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 18 Feb 2024 08:42:36 +0100 Subject: [PATCH 01/24] Tag Folders: add tag folder sorting and enabling - make tags sortable per drag&drop (then sorted everywhere) - each tag can individually be enabled as folder - fix redraw of tags/entity list on tag changes --- public/css/tags.css | 46 +++++++++++++++ public/index.html | 2 + public/script.js | 5 +- public/scripts/tags.js | 123 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 168 insertions(+), 8 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 3ad18c468..9d11c45a0 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -174,3 +174,49 @@ 1px -1px 0px black; opacity: 1; } + +.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); +} + +.tag_as_folder.yes_folder:after { + position: absolute; + top: -8px; + bottom: 0; + left: 0; + right: -24px; + 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, + -1px -1px 0px black, + -1px 1px 0px black, + 1px -1px 0px black; + opacity: 1; +} + +.tag_as_folder.no_folder:after { + position: absolute; + top: -8px; + bottom: 0; + left: 0; + right: -24px; + 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; +} diff --git a/public/index.html b/public/index.html index e91ea5cae..1252037b7 100644 --- a/public/index.html +++ b/public/index.html @@ -4542,6 +4542,8 @@
+
+
diff --git a/public/script.js b/public/script.js index dcab37d2a..a0c793f7b 100644 --- a/public/script.js +++ b/public/script.js @@ -163,6 +163,7 @@ import { renameTagKey, importTags, tag_filter_types, + compareTagsForSort, } from './scripts/tags.js'; import { SECRET_KEYS, @@ -1326,7 +1327,7 @@ export function getEntitiesList({ doFilter } = {}) { 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(x => x.is_folder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []), ]; if (doFilter) { @@ -1337,7 +1338,7 @@ export function getEntitiesList({ doFilter } = {}) { // 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 otherTags = tags.filter(x => x.is_folder && !filterData.selected.includes(x.id)).sort(compareTagsForSort); const bogusTags = []; for (const entity of entities) { for (const tag of otherTags) { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index f6a756278..29163a34b 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -12,7 +12,8 @@ import { import { FILTER_TYPES, 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 } from './utils.js'; +import { power_user } from './power-user.js'; export { tags, @@ -24,6 +25,8 @@ export { createTagMapFromList, renameTagKey, importTags, + sortTags, + compareTagsForSort, }; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; @@ -111,7 +114,7 @@ function getTagsList(key) { return tag_map[key] .map(x => tags.find(y => y.id === x)) .filter(x => x) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort(compareTagsForSort); } function getInlineListSelector() { @@ -245,7 +248,9 @@ async function importTags(imported_char) { } else { selected_tags = await callPopup(`

Importing Tags For ${imported_char.name}

${existingTags.length} existing tags have been found${existingTagsString}.

The following ${newTags.length} new tags will be imported.

`, 'input', newTags.join(', ')); } + // @ts-ignore selected_tags = existingTags.concat(selected_tags.split(',')); + // @ts-ignore selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== ''); //Anti-troll measure if (selected_tags.length > 15) { @@ -276,6 +281,8 @@ function createNewTag(tagName) { const tag = { id: uuidv4(), name: tagName, + is_folder: false, + sort_order: tags.length, color: '', color2: '', create_date: Date.now(), @@ -379,7 +386,7 @@ function printTagFilters(type = tag_filter_types.character) { const characterTagIds = Object.values(tag_map).flat(); const tagsToDisplay = tags .filter(x => characterTagIds.includes(x.id)) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort(compareTagsForSort); for (const tag of Object.values(ACTIONABLE_TAGS)) { appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true }); @@ -426,13 +433,16 @@ function onTagRemoveClick(event) { saveSettingsDebounced(); } +// @ts-ignore function onTagInput(event) { let val = $(this).val(); if (tags.find(t => t.name === val)) return; + // @ts-ignore $(this).autocomplete('search', val); } function onTagInputFocus() { + // @ts-ignore $(this).autocomplete('search', $(this).val()); } @@ -475,6 +485,7 @@ function applyTagsOnGroupSelect() { export function createTagInput(inputSelector, listSelector) { $(inputSelector) + // @ts-ignore .autocomplete({ source: (i, o) => findTag(i, o, listSelector), select: (e, u) => selectTag(e, u, listSelector), @@ -509,19 +520,94 @@ function onViewTagsListClick() {
+ 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.
`); - const sortedTags = tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase())); + const tagContainer = $('
'); + 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); + for (const tag of sortedTags) { - appendViewTagToList(list, tag, everything); + // // 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); } + makeTagListDraggable(tagContainer); + callPopup(list, 'text'); } +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()); + } +} + async function onTagRestoreFileSelect(e) { const file = e.target.files[0]; @@ -620,7 +706,7 @@ function onTagsBackupClick() { function onTagCreateClick() { const tag = createNewTag('New Tag'); - appendViewTagToList($('#tag_view_list'), tag, []); + appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []); printCharacters(false); saveSettingsDebounced(); } @@ -636,9 +722,15 @@ function appendViewTagToList(list, tag, everything) { template.find('.tag_view_name').css('background-color', tag.color); template.find('.tag_view_name').css('color', tag.color2); + const tagAsFolderId = tag.id + '-tag-folder'; const colorPickerId = tag.id + '-tag-color'; const colorPicker2Id = tag.id + '-tag-color2'; + 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'); template.find('.tagColorPickerHolder').html( ``, ); @@ -646,6 +738,7 @@ function appendViewTagToList(list, tag, everything) { ``, ); + template.find('.tag_as_folder').attr('id', tagAsFolderId); template.find('.tag-color').attr('id', colorPickerId); template.find('.tag-color2').attr('id', colorPicker2Id); @@ -663,10 +756,26 @@ function appendViewTagToList(list, tag, everything) { }); }, 100); + // @ts-ignore $(colorPickerId).color = tag.color; + // @ts-ignore $(colorPicker2Id).color = tag.color2; } +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(); + +} + function onTagDeleteClick() { if (!confirm('Are you sure?')) { return; @@ -738,8 +847,10 @@ jQuery(() => { $(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); $(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); }); + From 25a0ea0cb6ef0df0f6f8836f3b832332ca7c9b0f Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 19 Feb 2024 03:15:45 +0100 Subject: [PATCH 02/24] Tag Folders: tag filters indicator and show settings - Add an indicator if any tag filters are applied, so you can see if there are any filters even if the list is collapsed - Save collapse state of the tag list - Fix folders vanishing if tag filters are applied (now really) --- public/css/tags.css | 28 ++++++++++++++++++++-------- public/scripts/filters.js | 5 ++++- public/scripts/power-user.js | 1 + public/scripts/tags.js | 23 ++++++++++++++++++++++- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 9d11c45a0..7cdfda02f 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -187,10 +187,8 @@ .tag_as_folder.yes_folder:after { position: absolute; - top: -8px; - bottom: 0; - left: 0; - right: -24px; + top: calc(var(--mainFontSize) * -0.5); + right: calc(var(--mainFontSize) * -0.5); content: "\2714"; font-size: calc(var(--mainFontSize) * 1); color: green; @@ -205,10 +203,8 @@ .tag_as_folder.no_folder:after { position: absolute; - top: -8px; - bottom: 0; - left: 0; - right: -24px; + top: calc(var(--mainFontSize) * -0.5); + right: calc(var(--mainFontSize) * -0.5); content: "\2715"; font-size: calc(var(--mainFontSize) * 1); color: red; @@ -220,3 +216,19 @@ 1px -1px 0px black; opacity: 1; } + +.tag.indicator:after { + position: absolute; + top: calc(var(--mainFontSize) * -0.5); + right: -2px; + content: "\25CF"; + font-size: calc(var(--mainFontSize) * 1); + color: var(--SmartThemeBodyColor); + 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; +} diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 4a0efc3fc..2f505ec93 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -116,6 +116,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 +124,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; diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index c2d7a3634..94934c8c0 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -234,6 +234,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 29163a34b..fbb87280d 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -370,6 +370,7 @@ function onTagFilterClick(listElement) { } runTagFilters(listElement); + updateTagFilterIndicator(); } function runTagFilters(listElement) { @@ -407,6 +408,21 @@ function printTagFilters(type = tag_filter_types.character) { for (const tagId of selectedTagIds) { $(`${FILTER_SELECTOR} .tag[id="${tagId}"]`).trigger('click'); } + + if (power_user.show_tag_filters) { + $('.rm_tag_controls .showTagList').addClass('selected'); + $('.rm_tag_controls').find('.tag:not(.actionable)').show(); + } + + updateTagFilterIndicator(); +} + +function updateTagFilterIndicator() { + if ($('.rm_tag_controls').find('.tag:not(.actionable)').is('.selected, .excluded')) { + $('.rm_tag_controls .showTagList').addClass('indicator'); + } else { + $('.rm_tag_controls .showTagList').removeClass('indicator'); + } } function onTagRemoveClick(event) { @@ -829,10 +845,15 @@ function onTagColorize2(evt) { } function onTagListHintClick() { - console.log($(this)); + console.debug($(this)); $(this).toggleClass('selected'); $(this).siblings('.tag:not(.actionable)').toggle(100); $(this).siblings('.innerActionable').toggleClass('hidden'); + + power_user.show_tag_filters = $(this).hasClass('selected'); + saveSettingsDebounced(); + + console.log('show_tag_filters', power_user.show_tag_filters); } jQuery(() => { From 3e44dddfda3b7cc3d948159b7c85662f05a525d2 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 19 Feb 2024 05:30:42 +0100 Subject: [PATCH 03/24] Tag Folders: Drilldown for select and improved filter - drilldown visible for bogus folder selections - drilldown can be changed and refreshed - enhanced filters (remove empty folders/tags from list by default) --- public/css/tags.css | 22 ++++++++++++ public/index.html | 1 + public/script.js | 79 +++++++++++++++++++++++++----------------- public/scripts/tags.js | 8 +++++ 4 files changed, 79 insertions(+), 31 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 7cdfda02f..8c36ee5b4 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -124,6 +124,7 @@ display: flex; column-gap: 10px; flex-direction: row; + flex-wrap: wrap; align-items: flex-start; margin: 5px; } @@ -232,3 +233,24 @@ 1px -1px 0px black; opacity: 1; } + +.rm_tag_bogus_drilldown .tag:not(:first-child) { + position: relative; + margin-left: calc(var(--mainFontSize) * 2); +} + +.rm_tag_bogus_drilldown .tag:not(:first-child):before { + position: absolute; + left: calc(var(--mainFontSize) * -2); + top: -1px; + content: "\21E8"; + font-size: calc(var(--mainFontSize) * 2); + color: var(--SmartThemeBodyColor); + 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; +} diff --git a/public/index.html b/public/index.html index 1252037b7..1f3b24539 100644 --- a/public/index.html +++ b/public/index.html @@ -4210,6 +4210,7 @@
+

diff --git a/public/script.js b/public/script.js index a0c793f7b..f730c18c9 100644 --- a/public/script.js +++ b/public/script.js @@ -272,6 +272,7 @@ export { printCharacters, isOdd, countOccurrences, + chooseBogusFolder, }; showLoader(); @@ -1334,21 +1335,20 @@ export function getEntitiesList({ doFilter } = {}) { 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 => x.is_folder && !filterData.selected.includes(x.id)).sort(compareTagsForSort); - const bogusTags = []; - for (const entity of entities) { - for (const tag of otherTags) { - if (!bogusTags.includes(tag) && entitiesFilter.isElementTagged(entity, tag.id)) { - bogusTags.push(tag); - } + const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); + + entities = entities.filter(entity => { + 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)); } - entities.push(...bogusTags.map(item => tagToEntity(item))); - } + return true; + }); sortEntitiesList(entities); return entities; @@ -8054,6 +8054,40 @@ function doTogglePanels() { $('#option_settings').trigger('click'); } +function chooseBogusFolder(tagId, remove = false) { + // Update bogus filter + 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 if (remove) { + const index = filterData.selected.indexOf(tagId); + if (index > -1) filterData.selected.splice(index, 1); + } else { + filterData.selected.push(tagId); + filterData.bogus = true; + } + + entitiesFilter.setFilterData(FILTER_TYPES.TAG, filterData); + + // Update bogus drilldown + if (tagId === 'back') { + $('.rm_tag_controls .rm_tag_bogus_drilldown .tag').last().remove(); + } else if (remove) { + $(`.rm_tag_controls .rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove(); + } else { + const tag = tags.find(x => x.id === tagId); + appendTagToList('.rm_tag_controls .rm_tag_bogus_drilldown', tag, { removable: true, selectable: false, isGeneralList: false }); + } +} + function addDebugFunctions() { const doBackfill = async () => { for (const message of chat) { @@ -8229,24 +8263,7 @@ 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); + chooseBogusFolder(tagId); }); $(document).on('input', '.edit_textarea', function () { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index fbb87280d..82588166d 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -7,6 +7,7 @@ import { getCharacters, entitiesFilter, printCharacters, + chooseBogusFolder, } from '../script.js'; // eslint-disable-next-line no-unused-vars import { FILTER_TYPES, FilterHelper } from './filters.js'; @@ -430,6 +431,13 @@ function onTagRemoveClick(event) { const tag = $(this).closest('.tag'); const tagId = tag.attr('id'); + // Check if we are inside the drilldown. If so, we call remove on the bogus folder + if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) { + console.debug('Bogus drilldown remove', tagId); + chooseBogusFolder(tagId, true); + return; + } + // 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; From 1faf8b7ee27884f50ba5061b75b87c336619fc8e Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Tue, 27 Feb 2024 23:32:21 +0100 Subject: [PATCH 04/24] Tag Folders: Sync tag filters and bogus drilldown - When you select or unselect a filter that is also a folder, the folder gets put into the drilldown - When you click a folder or go back, the tag selection is synced accordingly - Do not throw away bogus/tag filter on redraw/refresh of the list --- public/script.js | 52 +++++++++++++++++++----------------------- public/scripts/tags.js | 18 ++++++++++++--- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/public/script.js b/public/script.js index f730c18c9..7cf3e08e6 100644 --- a/public/script.js +++ b/public/script.js @@ -1242,11 +1242,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); } @@ -1309,7 +1304,11 @@ async function printCharacters(fullRefresh = false) { * @returns {boolean} If currently viewing a folder */ function isBogusFolderOpen() { - return !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus; + const anyIsFolder = entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.selected + .map(tagId => tags.find(x => x.id === tagId)) + .some(x => !!x.is_folder); + + return !!anyIsFolder; } export function getEntitiesList({ doFilter } = {}) { @@ -8054,37 +8053,34 @@ function doTogglePanels() { $('#option_settings').trigger('click'); } -function chooseBogusFolder(tagId, remove = false) { - // Update bogus filter +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 = []; - filterData.bogus = false; } - if (tagId === 'back') { - filterData.selected.pop(); - filterData.bogus = filterData.selected.length > 0; - } else if (remove) { - const index = filterData.selected.indexOf(tagId); - if (index > -1) filterData.selected.splice(index, 1); - } else { - filterData.selected.push(tagId); - filterData.bogus = true; + 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; + remove = true; } - entitiesFilter.setFilterData(FILTER_TYPES.TAG, filterData); - - // Update bogus drilldown - if (tagId === 'back') { - $('.rm_tag_controls .rm_tag_bogus_drilldown .tag').last().remove(); - } else if (remove) { - $(`.rm_tag_controls .rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove(); + // Instead of manually updating the filter conditions, we just "click" on the filter tag + // We search inside which filter block we are located in and use that one + const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter'); + if (remove) { + // Click twice to skip over the 'excluded' state + $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click').trigger('click'); } else { - const tag = tags.find(x => x.id === tagId); - appendTagToList('.rm_tag_controls .rm_tag_bogus_drilldown', tag, { removable: true, selectable: false, isGeneralList: false }); + $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click'); } } @@ -8263,7 +8259,7 @@ jQuery(async function () { $(document).on('click', '.bogus_folder_select', function () { const tagId = $(this).attr('tagid'); console.log('Bogus folder clicked', tagId); - chooseBogusFolder(tagId); + chooseBogusFolder($(this), tagId); }); $(document).on('input', '.edit_textarea', function () { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 82588166d..f1ce12beb 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -345,6 +345,9 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe } function onTagFilterClick(listElement) { + const tagId = $(this).attr('id'); + const existingTag = tags.find((tag) => tag.id === tagId); + let excludeTag; if ($(this).hasClass('selected')) { $(this).removeClass('selected'); @@ -361,8 +364,6 @@ function onTagFilterClick(listElement) { // 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; @@ -370,6 +371,16 @@ function onTagFilterClick(listElement) { } } + // Update bogus folder if applicable + if (existingTag?.is_folder) { + // Update bogus drilldown + if ($(this).hasClass('selected')) { + appendTagToList('.rm_tag_controls .rm_tag_bogus_drilldown', existingTag, { removable: true, selectable: false, isGeneralList: false }); + } else { + $(listElement).closest('.rm_tag_controls').find(`.rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove(); + } + } + runTagFilters(listElement); updateTagFilterIndicator(); } @@ -385,6 +396,7 @@ function printTagFilters(type = tag_filter_types.character) { const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR; const selectedTagIds = [...($(FILTER_SELECTOR).find('.tag.selected').map((_, el) => $(el).attr('id')))]; $(FILTER_SELECTOR).empty(); + $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown').empty(); const characterTagIds = Object.values(tag_map).flat(); const tagsToDisplay = tags .filter(x => characterTagIds.includes(x.id)) @@ -434,7 +446,7 @@ function onTagRemoveClick(event) { // Check if we are inside the drilldown. If so, we call remove on the bogus folder if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) { console.debug('Bogus drilldown remove', tagId); - chooseBogusFolder(tagId, true); + chooseBogusFolder($(this), tagId, true); return; } From c0e112d1954da6ad640278fd80595130ed2285ab Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 28 Feb 2024 06:05:04 +0100 Subject: [PATCH 05/24] Tag Folders: inline info for groups and avatars - Add list of character avatars to folders (overflow hidden) - Add and/or move count of characters for both groups and folders in overview - Add name list of all chars for groups, above tags - Replace alt texts of all avatar images with the entity name - Made title/mouseover tooltip more useful with separation between types and their names - refactored CSS usage of avatar sizes to global variables - grid view alignment changes --- public/css/tags.css | 1 + public/css/toggle-dependent.css | 86 ++++++++++++++++++++++---------- public/index.html | 22 ++++++--- public/script.js | 45 +++++++++++++++-- public/scripts/group-chats.js | 22 +++++++-- public/scripts/tags.js | 6 --- public/style.css | 88 ++++++++++++++++++++++++--------- 7 files changed, 202 insertions(+), 68 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 8c36ee5b4..242ee4cbc 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -254,3 +254,4 @@ 1px -1px 0px black; opacity: 1; } + diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index e405b0eca..6e3c440d0 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -1,3 +1,9 @@ +:root { + --big-avatar-height-factor: 1.8; + --big-avatar-width-factor: 1.2; + --big-avatar-border-factor: 5; +} + body.tts .mes[is_system="true"] .mes_narrate { display: none; } @@ -21,7 +27,7 @@ body.square-avatars .avatar, body.square-avatars .avatar img, body.square-avatars .hotswapAvatar, body.square-avatars .hotswapAvatar img { - border-radius: 2px !important; + border-radius: var(--avatar-base-border-radius) !important; } /*char list grid mode*/ @@ -89,10 +95,12 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block { width: 100%; } -body.charListGrid #rm_print_characters_block .bogus_folder_counter_block, body.charListGrid #rm_print_characters_block .ch_description, body.charListGrid #rm_print_characters_block .tags_inline, body.charListGrid #rm_print_characters_block .character_version, +body.charListGrid #rm_print_characters_block .bogus_folder_unit, +body.charListGrid #rm_print_characters_block .group_select_unit, +body.charListGrid #rm_print_characters_block .group_select_block_list, body.charListGrid #rm_print_characters_block .ch_avatar_url, #user_avatar_block.gridView .avatar-container .ch_description { display: none; @@ -106,12 +114,12 @@ body.big-avatars .bogus_folder_select .avatar { } body:not(.big-avatars) .avatar { - border-radius: 50%; + border-radius: var(--avatar-base-border-radius-round); } body.big-avatars .avatar { - width: 60px; - height: 90px; + width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); /* width: unset; */ border-style: none; display: flex; @@ -120,33 +128,33 @@ body.big-avatars .avatar { align-items: center; /* align-self: unset; */ overflow: visible; - border-radius: 10px; + border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)); flex: 1 } body.big-avatars #user_avatar_block .avatar, body.big-avatars #user_avatar_block .avatar_upload { - height: 90px; - width: 60px; - border-radius: 10px; + width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); + border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)); } body.big-avatars #user_avatar_block .avatar img { - height: 90px; - width: 60px; + width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); } body.big-avatars .avatar img { - width: 60px; - height: 90px; + width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); object-fit: cover; object-position: center; border: 1px solid var(--SmartThemeBorderColor); - border-radius: 10px; + border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)); } body:not(.big-avatars) .avatar_collage { - min-width: 50px; + min-width: var(--avatar-base-width); aspect-ratio: 1 / 1; } @@ -155,8 +163,8 @@ body:not(.big-avatars) .avatar_collage img { } body.big-avatars .avatar_collage { - min-width: 60px; - max-width: 60px; + min-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); + max-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); aspect-ratio: 2 / 3; } @@ -169,42 +177,68 @@ body.big-avatars .avatar-container .ch_description { text-overflow: unset; } +body.big-avatars .avatars_inline .avatar, +body.big-avatars .avatars_inline .avatar img { + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-factor)); + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-factor)); +} + +body.big-avatars .bogus_folder_avatars_block { + flex-wrap: wrap; + overflow: hidden; + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-factor) + 2 * var(--avatar-base-border-radius)); + /* margin-top: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-factor) * 0.2); */ +} + +body.big-avatars .bogus_folder_avatars_block .avatar { + margin: calc(var(--avatar-base-border-radius)) 0; +} + +body:not(.big-avatars) .avatars_inline .avatar_collage { + min-width: calc(var(--avatar-base-width) * var(--inline-avatar-factor)); +} + +body.big-avatars .avatars_inline .avatar_collage { + min-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-factor)); + max-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-factor)); +} + /* border radius for big avatars collages */ body.big-avatars .collage_2 .img_1 { - border-radius: 10px 0 0 10px !important; + border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) !important; } body.big-avatars .collage_2 .img_2 { - border-radius: 0 10px 10px 0 !important; + border-radius: 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 !important; } body.big-avatars .collage_3 .img_1 { - border-radius: 10px 0 0 0 !important; + border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 0 0 !important; } body.big-avatars .collage_3 .img_2 { - border-radius: 0 10px 0 0 !important; + border-radius: 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 0 !important; } body.big-avatars .collage_3 .img_3 { - border-radius: 0 0 10px 10px !important; + border-radius: 0 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) !important; } body.big-avatars .collage_4 .img_1 { - border-radius: 10px 0 0 0 !important; + border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 0 0 !important; } body.big-avatars .collage_4 .img_2 { - border-radius: 0 10px 0 0 !important; + border-radius: 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 0 !important; } body.big-avatars .collage_4 .img_3 { - border-radius: 0 0 0 10px !important; + border-radius: 0 0 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) !important; } body.big-avatars .collage_4 .img_4 { - border-radius: 0 0 10px 0 !important; + border-radius: 0 0 calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)) 0 !important; } diff --git a/public/index.html b/public/index.html index 1f3b24539..2d1e117d7 100644 --- a/public/index.html +++ b/public/index.html @@ -5006,10 +5006,16 @@
-
-
+
+
+
+ group of + 5 + characters +
+
@@ -5022,14 +5028,18 @@
+ + characters
-
- - character card(s) -
+
+
+
+ +
+
diff --git a/public/script.js b/public/script.js index 7cf3e08e6..453665bb6 100644 --- a/public/script.js +++ b/public/script.js @@ -1159,18 +1159,53 @@ export async function selectCharacterById(id) { function getTagBlock(item, entities) { let count = 0; + let subEntities = []; for (const entity of entities) { if (entitiesFilter.isElementTagged(entity, item.id)) { count++; + subEntities.push(entity); } } const template = $('#bogus_folder_template .bogus_folder_select').clone(); template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` }); - template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 }); - template.find('.ch_name').text(item.name); + 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('.bogus_folder_counter').text(count); + + // Fill inline character images + const inlineAvatars = template.find('.bogus_folder_avatars_block'); + for (const entitiy of subEntities) { + const id = entitiy.id; + + // Populate the template + const avatarTemplate = $('#bogus_folder_inline_character_template .avatar').clone(); + + let this_avatar = default_avatar; + if (entitiy.item.avatar != 'none') { + this_avatar = getThumbnailUrl('avatar', entitiy.item.avatar); + } + + avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); + avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entitiy.item.name); + avatarTemplate.attr('title', `[Character] ${entitiy.item.name}`); + avatarTemplate.toggleClass('is_fav', entitiy.item.fav || entitiy.item.fav == 'true'); + avatarTemplate.find('.ch_fav').val(entitiy.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 (entitiy.type === 'group') { + const grpTemplate = getGroupAvatar(entitiy.item); + + avatarTemplate.addClass(grpTemplate.attr('class')); + avatarTemplate.empty(); + avatarTemplate.append(grpTemplate.children()); + avatarTemplate.attr('title', `Group: ${entitiy.item.name}`); + } + + inlineAvatars.append(avatarTemplate); + } + return template; } @@ -1200,9 +1235,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); } diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index bb0775624..5d9604aac 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -523,13 +523,27 @@ 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) { + count++; + const character = characters.find(x => x.avatar === member || x.name === member); + namesList.push(`${character.name}`); + } + } + 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); + template.find('.group_select_block_list').append(namesList.join('')); // Display inline tags const tags = getTagsList(group.id); @@ -565,11 +579,11 @@ function isValidImageUrl(url) { function getGroupAvatar(group) { if (!group) { - return $(`
`); + return $(`
`); } // if isDataURL or if it's a valid local file url if (isValidImageUrl(group.avatar_url)) { - return $(`
`); + return $(`
`); } const memberAvatars = []; @@ -595,6 +609,7 @@ function getGroupAvatar(group) { groupAvatar.find(`.img_${i + 1}`).attr('src', memberAvatars[i]); } + groupAvatar.attr('title', `[Group] ${group.name}`); return groupAvatar; } @@ -606,6 +621,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; } diff --git a/public/scripts/tags.js b/public/scripts/tags.js index f1ce12beb..d54d2af26 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -567,14 +567,8 @@ function onViewTagsListClick() { 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); 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); } diff --git a/public/style.css b/public/style.css index c8befaf63..f70eb433c 100644 --- a/public/style.css +++ b/public/style.css @@ -96,6 +96,12 @@ /*styles for the color picker*/ --tool-cool-color-picker-btn-bg: transparent; --tool-cool-color-picker-btn-border-color: transparent; + + --avatar-base-height: 50px; + --avatar-base-width: 50px; + --avatar-base-border-radius: 2px; + --avatar-base-border-radius-round: 50%; + --inline-avatar-factor: 0.6; } * { @@ -849,8 +855,8 @@ hr { } .add_avatar { - border: 2px solid var(--SmartThemeBodyColor); - margin: 2px; + border: var(--avatar-base-border-radius) solid var(--SmartThemeBodyColor); + margin: var(--avatar-base-border-radius); cursor: pointer; transition: filter 0.2s ease-in-out; } @@ -860,14 +866,14 @@ hr { } .avatar { - width: 50px; - height: 50px; + width: var(--avatar-base-width); + height: var(--avatar-base-height); border-style: none; flex: 1; } .last_mes .mesAvatarWrapper { - padding-bottom: 50px; + padding-bottom: var(--avatar-base-height); } .mes .avatar { @@ -880,8 +886,8 @@ hr { .hotswapAvatar, .hotswapAvatar .avatar { - width: 50px !important; - height: 50px !important; + width: var(--avatar-base-width) !important; + height: var(--avatar-base-height) !important; border-style: none; } @@ -901,10 +907,10 @@ hr { .hotswapAvatar .avatar_collage, .hotswapAvatar.group_select { - border-radius: 50% !important; + border-radius: var(--avatar-base-border-radius-round) !important; position: relative; overflow: hidden; - min-width: 50px !important; + min-width: var(--avatar-base-width) !important; } .hotswapAvatar.group_select .avatar.avatar_collage img { @@ -916,21 +922,21 @@ hr { } .hotswapAvatar .avatar { - width: 50px !important; - height: 50px !important; + width: var(--avatar-base-width) !important; + height: var(--avatar-base-height) !important; object-fit: cover; object-position: center center; - border-radius: 50% !important; + border-radius: var(--avatar-base-border-radius-round) !important; box-shadow: 0 0 5px var(--black50a); } .hotswapAvatar img, .avatar img { - width: 50px; - height: 50px; + width: var(--avatar-base-width); + height: var(--avatar-base-height); object-fit: cover; object-position: center center; - border-radius: 50%; + border-radius: var(--avatar-base-border-radius-round); border: 1px solid var(--SmartThemeBorderColor); /*--black30a*/ box-shadow: 0 0 5px var(--black50a); @@ -950,6 +956,43 @@ hr { outline-color: var(--SmartThemeBorderColor); } +.avatars_inline .avatar, +.avatars_inline .avatar img { + width: calc(var(--avatar-base-width) * var(--inline-avatar-factor)); + height: calc(var(--avatar-base-height) * var(--inline-avatar-factor)); +} + +.bogus_folder_avatars_block { + 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_avatars_block .avatar { + margin: calc(var(--avatar-base-border-radius)) 0; +} + +.group_select_block_list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + gap: 0.2rem; + align-items: flex-end; + flex-basis: 100%; +} + +.group_select_block_list .group_ch_name:not(:last-child):not(:nth-last-child(2)):after { + content: ", "; + font-size: calc(var(--mainFontSize) * 0.9); +} + +.group_select_block_list .group_ch_name:nth-last-child(2):after { + content: " & "; + font-size: calc(var(--mainFontSize) * 0.9); +} + .mes_block { padding-top: 0; padding-left: 10px; @@ -1778,7 +1821,8 @@ input[type=search]:focus::-webkit-search-cancel-button { .character_select.is_fav .avatar, .group_select.is_fav .avatar, -.group_member.is_fav .avatar { +.group_member.is_fav .avatar, +.avatar.is_fav { outline: 2px solid var(--golden); } @@ -1842,8 +1886,8 @@ input[type=search]:focus::-webkit-search-cancel-button { cursor: pointer; margin-bottom: 1px; width: 100%; - outline: 2px solid transparent; - border: 2px solid transparent; + outline: var(--avatar-base-border-radius) solid transparent; + border: var(--avatar-base-border-radius) solid transparent; } .avatar-container .character_select_container { @@ -2297,18 +2341,18 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button .avatar-container .avatar { cursor: pointer; - border-radius: 50%; + border-radius: var(--avatar-base-border-radius-round); align-self: center; - outline: 2px solid transparent; + outline: var(--avatar-base-border-radius) solid transparent; flex: unset; } .avatar-container.selected { - border: 2px solid rgba(255, 255, 255, 0.7); + border: var(--avatar-base-border-radius) solid rgba(255, 255, 255, 0.7); } .avatar-container.default_persona .avatar { - outline: 2px solid var(--golden); + outline: var(--avatar-base-border-radius) solid var(--golden); } .avatar-container.default_persona .set_default_persona { From e578d3dbb6a6f3eb7b3f1d642b6a30e08eacda4b Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 6 Mar 2024 00:28:14 +0100 Subject: [PATCH 06/24] 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 --- public/css/tags.css | 35 +++------- public/index.html | 10 +-- public/script.js | 81 +++++++++++------------ public/scripts/group-chats.js | 3 + public/scripts/tags.js | 119 +++++++++++++++++++++++++++++++--- public/style.css | 5 +- 6 files changed, 173 insertions(+), 80 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 242ee4cbc..a278d941c 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -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; diff --git a/public/index.html b/public/index.html index 2d1e117d7..690746450 100644 --- a/public/index.html +++ b/public/index.html @@ -4544,7 +4544,9 @@
-
+
+ +
@@ -5011,7 +5013,7 @@
group of 5 - characters + characters
@@ -5023,13 +5025,13 @@
- +
- characters + characters
diff --git a/public/script.js b/public/script.js index 453665bb6..3ff831e66 100644 --- a/public/script.js +++ b/public/script.js @@ -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); }); diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 5d9604aac..86e364c78 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -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); diff --git a/public/scripts/tags.js b/public/scripts/tags.js index d54d2af26..b42bce191 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -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( ``, ); @@ -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(() => { diff --git a/public/style.css b/public/style.css index f70eb433c..9dc47a65b 100644 --- a/public/style.css +++ b/public/style.css @@ -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 { From 4ab45330c4cfddfeb7a4bc362fad590046e86a55 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 6 Mar 2024 01:05:08 +0100 Subject: [PATCH 07/24] Tag Folders: Refactor tag code into tag.js --- public/script.js | 100 ++---------------------------------- public/scripts/tags.js | 112 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 99 deletions(-) diff --git a/public/script.js b/public/script.js index 3ff831e66..75f924c01 100644 --- a/public/script.js +++ b/public/script.js @@ -153,10 +153,13 @@ 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, + isBogusFolder, + isBogusFolderOpen, + chooseBogusFolder, + getTagBlock, loadTagsSettings, printTagFilters, getTagsList, @@ -166,7 +169,6 @@ import { importTags, tag_filter_types, compareTagsForSort, - TAG_FOLDER_TYPES, } from './scripts/tags.js'; import { SECRET_KEYS, @@ -275,8 +277,6 @@ export { printCharacters, isOdd, countOccurrences, - chooseBogusFolder, - isBogusFolder, }; showLoader(); @@ -1161,57 +1161,6 @@ export async function selectCharacterById(id) { } } -function getTagBlock(item, entities) { - let count = entities.length; - - 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('.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 entities) { - const id = entitiy.id; - - // Populate the template - const avatarTemplate = $('#bogus_folder_inline_character_template .avatar').clone(); - - let this_avatar = default_avatar; - if (entitiy.item.avatar != 'none') { - this_avatar = getThumbnailUrl('avatar', entitiy.item.avatar); - } - - avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); - avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entitiy.item.name); - avatarTemplate.attr('title', `[Character] ${entitiy.item.name}`); - avatarTemplate.toggleClass('is_fav', entitiy.item.fav || entitiy.item.fav == 'true'); - avatarTemplate.find('.ch_fav').val(entitiy.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 (entitiy.type === 'group') { - const grpTemplate = getGroupAvatar(entitiy.item); - - avatarTemplate.addClass(grpTemplate.attr('class')); - avatarTemplate.empty(); - avatarTemplate.append(grpTemplate.children()); - avatarTemplate.attr('title', `Group: ${entitiy.item.name}`); - } - - inlineAvatars.append(avatarTemplate); - } - - return template; -} - function getBackBlock() { const template = $('#bogus_folder_back_template .bogus_folder_select').clone(); return template; @@ -1337,26 +1286,6 @@ 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 - */ -function isBogusFolderOpen() { - const anyIsFolder = entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.selected - .map(tagId => tags.find(x => x.id === tagId)) - .some(isBogusFolder); - - return !!anyIsFolder; -} - export function getEntitiesList({ doFilter } = {}) { function characterToEntity(character, id) { return { item: character, id, type: 'character' }; @@ -8099,27 +8028,6 @@ function doTogglePanels() { $('#option_settings').trigger('click'); } -function chooseBogusFolder(source, tagId, remove = false) { - // 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) { - 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; - } - - // Instead of manually updating the filter conditions, we just "click" on the filter tag - // We search inside which filter block we are located in and use that one - const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter'); - if (remove) { - // Click twice to skip over the 'excluded' state - $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click').trigger('click'); - } else { - $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click'); - } -} - function addDebugFunctions() { const doBackfill = async () => { for (const message of chat) { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index b42bce191..93b78852d 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -7,13 +7,13 @@ import { getCharacters, entitiesFilter, printCharacters, - chooseBogusFolder, - isBogusFolder, + getThumbnailUrl, + default_avatar, } from '../script.js'; // eslint-disable-next-line no-unused-vars import { FILTER_TYPES, FilterHelper } from './filters.js'; -import { groupCandidatesFilter, groups, selected_group } from './group-chats.js'; +import { groupCandidatesFilter, groups, selected_group, getGroupAvatar } from './group-chats.js'; import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js'; import { power_user } from './power-user.js'; @@ -23,6 +23,10 @@ export { tags, tag_map, filterByTagState, + isBogusFolder, + isBogusFolderOpen, + chooseBogusFolder, + getTagBlock, loadTagsSettings, printTagFilters, getTagsList, @@ -142,7 +146,109 @@ function filterTagSubEntities(tag, entities) { return entities; } +/** + * 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 + */ +function isBogusFolderOpen() { + const anyIsFolder = entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.selected + .map(tagId => tags.find(x => x.id === tagId)) + .some(isBogusFolder); + + return !!anyIsFolder; +} + +/** + * Function to be called when a specific tag/folder is chosen to "drill down". + * @param {*} source The jQuery element clicked when choosing the folder + * @param {string} tagId The tag id that is behind the chosen folder + * @param {boolean} remove Whether the given tag should be removed (otherwise it is added/chosen) + */ +function chooseBogusFolder(source, tagId, remove = false) { + // 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) { + 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; + } + + // Instead of manually updating the filter conditions, we just "click" on the filter tag + // We search inside which filter block we are located in and use that one + const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter'); + if (remove) { + // Click twice to skip over the 'excluded' state + $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click').trigger('click'); + } else { + $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click'); + } +} + +function getTagBlock(item, entities) { + let count = entities.length; + + 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('.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 + buildTagInlineAvatars(template, entities); + + return template; +} + +function buildTagInlineAvatars(template, entities) { + const inlineAvatars = template.find('.bogus_folder_avatars_block'); + inlineAvatars.empty(); + + for (const entitiy of entities) { + const id = entitiy.id; + + // Populate the template + const avatarTemplate = $('#bogus_folder_inline_character_template .avatar').clone(); + + let this_avatar = default_avatar; + if (entitiy.item.avatar !== undefined && entitiy.item.avatar != 'none') { + this_avatar = getThumbnailUrl('avatar', entitiy.item.avatar); + } + + avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); + avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entitiy.item.name); + avatarTemplate.attr('title', `[Character] ${entitiy.item.name}`); + avatarTemplate.toggleClass('is_fav', entitiy.item.fav || entitiy.item.fav == 'true'); + avatarTemplate.find('.ch_fav').val(entitiy.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 (entitiy.type === 'group') { + const grpTemplate = getGroupAvatar(entitiy.item); + + avatarTemplate.addClass(grpTemplate.attr('class')); + avatarTemplate.empty(); + avatarTemplate.append(grpTemplate.children()); + avatarTemplate.attr('title', `Group: ${entitiy.item.name}`); + } + + inlineAvatars.append(avatarTemplate); + } +} /** * Applies the favorite filter to the character list. From fc6146fa0020d68cc115f1e6dd2807007d2e0243 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 6 Mar 2024 04:59:39 +0100 Subject: [PATCH 08/24] Tag Folders: Rework favorites display - Favorites display uses same method than inline avatars - Favorites now respect avatar style --- public/css/toggle-dependent.css | 38 ++++++-------- public/index.html | 15 ++---- public/script.js | 50 +++++++++++++++++- public/scripts/RossAscends-mods.js | 79 +++-------------------------- public/scripts/tags.js | 42 ++-------------- public/style.css | 81 ++++++++++++------------------ 6 files changed, 110 insertions(+), 195 deletions(-) diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 6e3c440d0..72af383d2 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -24,9 +24,7 @@ body.no-modelIcons .icon-svg { } body.square-avatars .avatar, -body.square-avatars .avatar img, -body.square-avatars .hotswapAvatar, -body.square-avatars .hotswapAvatar img { +body.square-avatars .avatar img { border-radius: var(--avatar-base-border-radius) !important; } @@ -113,10 +111,6 @@ body.big-avatars .bogus_folder_select .avatar { flex: unset; } -body:not(.big-avatars) .avatar { - border-radius: var(--avatar-base-border-radius-round); -} - body.big-avatars .avatar { width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); @@ -129,7 +123,6 @@ body.big-avatars .avatar { /* align-self: unset; */ overflow: visible; border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)); - flex: 1 } body.big-avatars #user_avatar_block .avatar, @@ -177,30 +170,27 @@ body.big-avatars .avatar-container .ch_description { text-overflow: unset; } -body.big-avatars .avatars_inline .avatar, -body.big-avatars .avatars_inline .avatar img { - width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-factor)); - height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-factor)); +body.big-avatars .avatars_inline_small .avatar, +body.big-avatars .avatars_inline_small .avatar img { + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-small-factor)); + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-small-factor)); } -body.big-avatars .bogus_folder_avatars_block { - flex-wrap: wrap; - overflow: hidden; - height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-factor) + 2 * var(--avatar-base-border-radius)); - /* margin-top: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-factor) * 0.2); */ +body.big-avatars .avatars_inline { + max-height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) + 2 * var(--avatar-base-border-radius)); } -body.big-avatars .bogus_folder_avatars_block .avatar { - margin: calc(var(--avatar-base-border-radius)) 0; +body.big-avatars .avatars_inline.avatars_inline_small { + height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-small-factor) + 2 * var(--avatar-base-border-radius)); } -body:not(.big-avatars) .avatars_inline .avatar_collage { - min-width: calc(var(--avatar-base-width) * var(--inline-avatar-factor)); +body:not(.big-avatars) .avatars_inline_small .avatar_collage { + min-width: calc(var(--avatar-base-width) * var(--inline-avatar-small-factor)); } -body.big-avatars .avatars_inline .avatar_collage { - min-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-factor)); - max-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-factor)); +body.big-avatars .avatars_inline_small .avatar_collage { + min-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-small-factor)); + max-width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor) * var(--inline-avatar-small-factor)); } /* border radius for big avatars collages */ diff --git a/public/index.html b/public/index.html index c2a9e7bf2..fa18f0764 100644 --- a/public/index.html +++ b/public/index.html @@ -3888,7 +3888,7 @@
-
+

@@ -5022,15 +5022,10 @@ characters
-
+
-
-
- -
-
@@ -5043,9 +5038,9 @@
-
-
- +
+
+
diff --git a/public/script.js b/public/script.js index 54f79d040..8d0648c86 100644 --- a/public/script.js +++ b/public/script.js @@ -249,6 +249,7 @@ export { scrollChatToBottom, isStreamingEnabled, getThumbnailUrl, + buildAvatarList, getStoppingStrings, reloadMarkdownProcessor, getCurrentChatId, @@ -1289,7 +1290,7 @@ async function printCharacters(fullRefresh = false) { favsToHotswap(); } -export function getEntitiesList({ doFilter } = {}) { +export function getEntitiesList({ doFilter = false, doSort = true } = {}) { function characterToEntity(character, id) { return { item: character, id, type: 'character' }; } @@ -1327,7 +1328,9 @@ export function getEntitiesList({ doFilter } = {}) { entities = filterByTagState(entities, { globalDisplayFilters: true }); } - sortEntitiesList(entities); + if (doSort) { + sortEntitiesList(entities); + } return entities; } @@ -5245,6 +5248,49 @@ function getThumbnailUrl(type, file) { return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; } +function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false } = {}) { + 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}`); + 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 { diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 73bd5b7bc..8c7d34c8c 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -12,6 +12,7 @@ import { setActiveCharacter, getEntitiesList, getThumbnailUrl, + buildAvatarList, selectCharacterById, eventSource, menu_type, @@ -264,82 +265,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 }); } //changes input bar and send button display depending on connection status diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 93b78852d..9f16d4db1 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -7,13 +7,12 @@ import { getCharacters, entitiesFilter, printCharacters, - getThumbnailUrl, - default_avatar, + buildAvatarList, } from '../script.js'; // eslint-disable-next-line no-unused-vars import { FILTER_TYPES, FilterHelper } from './filters.js'; -import { groupCandidatesFilter, groups, selected_group, getGroupAvatar } from './group-chats.js'; +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'; @@ -210,46 +209,11 @@ function getTagBlock(item, entities) { } // Fill inline character images - buildTagInlineAvatars(template, entities); + buildAvatarList(template.find('.bogus_folder_avatars_block'), entities); return template; } -function buildTagInlineAvatars(template, entities) { - const inlineAvatars = template.find('.bogus_folder_avatars_block'); - inlineAvatars.empty(); - - for (const entitiy of entities) { - const id = entitiy.id; - - // Populate the template - const avatarTemplate = $('#bogus_folder_inline_character_template .avatar').clone(); - - let this_avatar = default_avatar; - if (entitiy.item.avatar !== undefined && entitiy.item.avatar != 'none') { - this_avatar = getThumbnailUrl('avatar', entitiy.item.avatar); - } - - avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); - avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entitiy.item.name); - avatarTemplate.attr('title', `[Character] ${entitiy.item.name}`); - avatarTemplate.toggleClass('is_fav', entitiy.item.fav || entitiy.item.fav == 'true'); - avatarTemplate.find('.ch_fav').val(entitiy.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 (entitiy.type === 'group') { - const grpTemplate = getGroupAvatar(entitiy.item); - - avatarTemplate.addClass(grpTemplate.attr('class')); - avatarTemplate.empty(); - avatarTemplate.append(grpTemplate.children()); - avatarTemplate.attr('title', `Group: ${entitiy.item.name}`); - } - - inlineAvatars.append(avatarTemplate); - } -} - /** * Applies the favorite filter to the character list. * @param {FilterHelper} filterHelper Instance of FilterHelper class. diff --git a/public/style.css b/public/style.css index a370ad9a8..a79ea5fd7 100644 --- a/public/style.css +++ b/public/style.css @@ -101,7 +101,7 @@ --avatar-base-width: 50px; --avatar-base-border-radius: 2px; --avatar-base-border-radius-round: 50%; - --inline-avatar-factor: 0.6; + --inline-avatar-small-factor: 0.6; } * { @@ -868,8 +868,10 @@ hr { .avatar { width: var(--avatar-base-width); height: var(--avatar-base-height); + border-radius: var(--avatar-base-border-radius-round); border-style: none; flex: 1; + transition: 250ms; } .last_mes .mesAvatarWrapper { @@ -880,40 +882,21 @@ hr { cursor: pointer; } -#HotSwapWrapper .hotswap { - justify-content: space-evenly; +.hotswap { + margin: 5px; } -.hotswapAvatar, -.hotswapAvatar .avatar { - width: var(--avatar-base-width) !important; - height: var(--avatar-base-height) !important; - border-style: none; +.avatar.selectable { + opacity: 0.6; } -.hotswapAvatar { - opacity: 0.5; - transition: 250ms; - overflow: hidden; - padding: 0 !important; - order: 100; -} - -.hotswapAvatar:hover { +.avatar.selectable:hover { opacity: 1; background-color: transparent !important; cursor: pointer; } -.hotswapAvatar .avatar_collage, -.hotswapAvatar.group_select { - border-radius: var(--avatar-base-border-radius-round) !important; - position: relative; - overflow: hidden; - min-width: var(--avatar-base-width) !important; -} - -.hotswapAvatar.group_select .avatar.avatar_collage img { +.avatar.avatar_collage img { width: 100%; height: 100%; object-fit: cover; @@ -921,16 +904,6 @@ hr { border: 1px solid var(--SmartThemeBorderColor); } -.hotswapAvatar .avatar { - width: var(--avatar-base-width) !important; - height: var(--avatar-base-height) !important; - object-fit: cover; - object-position: center center; - border-radius: var(--avatar-base-border-radius-round) !important; - box-shadow: 0 0 5px var(--black50a); -} - -.hotswapAvatar img, .avatar img { width: var(--avatar-base-width); height: var(--avatar-base-height); @@ -943,10 +916,17 @@ hr { } .bogus_folder_select .avatar, -.character_select .avatar { +.character_select .avatar, +.avatars_inline .avatar { flex: unset; } +.avatars_inline { + flex-wrap: wrap; + overflow: hidden; + max-height: calc(var(--avatar-base-height) + 2 * var(--avatar-base-border-radius)); +} + .bogus_folder_select .avatar { justify-content: center; background-color: var(--SmartThemeBlurTintColor); @@ -956,24 +936,24 @@ hr { outline-color: var(--SmartThemeBorderColor); } -.avatars_inline .avatar, -.avatars_inline .avatar img { - width: calc(var(--avatar-base-width) * var(--inline-avatar-factor)); - height: calc(var(--avatar-base-height) * var(--inline-avatar-factor)); +.avatars_inline_small .avatar, +.avatars_inline_small .avatar img { + width: calc(var(--avatar-base-width) * var(--inline-avatar-small-factor)); + height: calc(var(--avatar-base-height) * var(--inline-avatar-small-factor)); } -.bogus_folder_avatars_block { - flex-wrap: wrap; - overflow: hidden; - height: calc(var(--avatar-base-height) * var(--inline-avatar-factor) + 2 * var(--avatar-base-border-radius)); +.avatars_inline_small { + height: calc(var(--avatar-base-height) * var(--inline-avatar-small-factor) + 2 * var(--avatar-base-border-radius)); } .bogus_folder_select:not(.folder_closed) .bogus_folder_avatars_block { opacity: 1 !important; } -.bogus_folder_avatars_block .avatar { - margin: calc(var(--avatar-base-border-radius)) 0; +.avatars_inline .avatar { + margin-top: calc(var(--avatar-base-border-radius)); + margin-left: calc(var(--avatar-base-border-radius)); + margin-bottom: calc(var(--avatar-base-border-radius)); } .group_select_block_list { @@ -1627,6 +1607,11 @@ input[type=search]:focus::-webkit-search-cancel-button { margin-bottom: 1px; } +.character_select.inline_avatar { + padding: unset; + border-radius: var(--avatar-base-border-radius-round); +} + /*applies to char list and mes_text char display name*/ .ch_name { @@ -3853,4 +3838,4 @@ a { height: 100vh; z-index: 9999; } -} \ No newline at end of file +} From 18379ec602d923e5d68e368265cb6df16c4b51bd Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 6 Mar 2024 23:13:22 +0100 Subject: [PATCH 09/24] Tag Folders: Improve global tag filters - Update global tag filters to three-state filters - Add filter for folders (showing empty folders or no folders) - Final fix of filtering (should be correct now) --- public/css/tags.css | 4 +- public/script.js | 17 +++++++- public/scripts/filters.js | 68 ++++++++++++++++++++++++++----- public/scripts/tags.js | 85 +++++++++++++++++++++++---------------- public/style.css | 3 ++ 5 files changed, 130 insertions(+), 47 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index a278d941c..0c127d643 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -160,11 +160,11 @@ .tag.excluded::after { position: absolute; - top: 0; - bottom: 0; + height: calc(var(--mainFontSize)*1.5); left: 0; right: 0; content: "\d7"; + pointer-events: none; font-size: calc(var(--mainFontSize) *3); color: red; line-height: calc(var(--mainFontSize)*1.3); diff --git a/public/script.js b/public/script.js index 8d0648c86..51cd8f5a9 100644 --- a/public/script.js +++ b/public/script.js @@ -1309,23 +1309,36 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) { ...(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 = entitiesFilter.applyFilters(entities); 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') { - entity.entities = filterByTagState(entities, { subForEntity: entity }); + let subEntities = filterByTagState(entities, { subForEntity: entity }); + if (doFilter) { + subEntities = entitiesFilter.applyFilters(subEntities); + } + entity.entities = subEntities; } } // Second run filters, hiding whatever should be filtered later if (doFilter) { entities = filterByTagState(entities, { globalDisplayFilters: true }); + entities = entitiesFilter.applyFilters(entities); } if (doSort) { diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 2f505ec93..1754d613d 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]: '', @@ -144,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 }); } /** @@ -157,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/tags.js b/public/scripts/tags.js index 9f16d4db1..255455ddd 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -10,7 +10,7 @@ import { 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, getSortableDelay } from './utils.js'; @@ -50,8 +50,9 @@ export const tag_filter_types = { }; 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' }, }; @@ -81,7 +82,11 @@ 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. + * @param {Array} entities List of entities for display, consisting of tags, characters and groups. + * @param {Object} param1 Optional parameters, explained below. + * @param {Boolean} [param1.globalDisplayFilters] When enabled, applies the final filter for the global list. Icludes filtering out entities in closed/hidden folders and empty folders. + * @param {Object} [param1.subForEntity] When given an entity, the list of entities gets filtered specifically for that one as a "sub list", filtering out other tags, elements not tagged for this and hidden elements. + * @returns The filtered list of entities */ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity = undefined } = {}) { const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); @@ -108,8 +113,9 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity } // Hide folders that have 0 visible sub entities after the first filtering round + const alwaysFolder = isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED); if (entity.type === 'tag') { - return entity.entities.length > 0; + return alwaysFolder || entity.entities.length > 0; } return true; @@ -218,12 +224,9 @@ function getTagBlock(item, entities) { * Applies the favorite filter to the character list. * @param {FilterHelper} filterHelper Instance of FilterHelper class. */ -function applyFavFilter(filterHelper) { - const isSelected = $(this).hasClass('selected'); - const displayFavoritesOnly = !isSelected; - $(this).toggleClass('selected', displayFavoritesOnly); - - filterHelper.setFilterData(FILTER_TYPES.FAV, displayFavoritesOnly); +function filterByFav(filterHelper) { + const state = toggleTagThreeState($(this)); + filterHelper.setFilterData(FILTER_TYPES.FAV, state); } /** @@ -231,11 +234,17 @@ function applyFavFilter(filterHelper) { * @param {FilterHelper} filterHelper Instance of FilterHelper class. */ function filterByGroups(filterHelper) { - const isSelected = $(this).hasClass('selected'); - const displayGroupsOnly = !isSelected; - $(this).toggleClass('selected', displayGroupsOnly); + const state = toggleTagThreeState($(this)); + filterHelper.setFilterData(FILTER_TYPES.GROUP, state); +} - filterHelper.setFilterData(FILTER_TYPES.GROUP, displayGroupsOnly); +/** + * Applies the "only folder" filter to the character list. + * @param {FilterHelper} filterHelper Instance of FilterHelper class. + */ +function filterByFolder(filterHelper) { + const state = toggleTagThreeState($(this)); + filterHelper.setFilterData(FILTER_TYPES.FOLDER, state); } function loadTagsSettings(settings) { @@ -475,7 +484,7 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe } if (tag.excluded && isGeneralList) { - $(tagElement).addClass('excluded'); + toggleTagThreeState(tagElement, FILTER_STATES.EXCLUDED); } if (selectable) { @@ -498,27 +507,13 @@ function onTagFilterClick(listElement) { const tagId = $(this).attr('id'); const existingTag = tags.find((tag) => tag.id === tagId); - let excludeTag; - if ($(this).hasClass('selected')) { - $(this).removeClass('selected'); - $(this).addClass('excluded'); - excludeTag = true; - } - else if ($(this).hasClass('excluded')) { - $(this).removeClass('excluded'); - excludeTag = false; - } - else { - $(this).addClass('selected'); - } + let state = toggleTagThreeState($(this)); // Manual undefined check required for three-state boolean - if (excludeTag !== undefined) { - if (existingTag) { - existingTag.excluded = excludeTag; + if (existingTag) { + existingTag.excluded = isFilterState(state, FILTER_STATES.EXCLUDED); - saveSettingsDebounced(); - } + saveSettingsDebounced(); } // Update bogus folder if applicable @@ -535,6 +530,28 @@ function onTagFilterClick(listElement) { updateTagFilterIndicator(); } +function toggleTagThreeState(element, stateOverride = undefined) { + const states = Object.keys(FILTER_STATES); + + const overrideKey = states.includes(stateOverride) ? stateOverride : states.find(key => FILTER_STATES[key] === stateOverride); + + const currentState = element.attr('data-toggle-state') ?? states[states.length - 1]; + const nextState = overrideKey ?? states[(states.indexOf(currentState) + 1) % states.length]; + + element.attr('data-toggle-state', nextState); + + console.debug('toggle three-way filter on', element, 'from', currentState, 'to', nextState); + + // Update css class and remove all others + Object.keys(FILTER_STATES).forEach(x => { + if (!isFilterState(x, FILTER_STATES.UNDEFINED)) { + element.toggleClass(FILTER_STATES[x].class, x === nextState); + } + }); + + return nextState; +} + function runTagFilters(listElement) { 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')))]; @@ -950,7 +967,7 @@ function onTagAsFolderClick() { // Cycle through folder types const types = Object.keys(TAG_FOLDER_TYPES); - let currentTypeIndex = types.indexOf(tag.folder_type); + const currentTypeIndex = types.indexOf(tag.folder_type); tag.folder_type = types[(currentTypeIndex + 1) % types.length]; updateDrawTagFolder(element, tag); diff --git a/public/style.css b/public/style.css index a79ea5fd7..bf9cad51a 100644 --- a/public/style.css +++ b/public/style.css @@ -955,6 +955,9 @@ hr { margin-left: calc(var(--avatar-base-border-radius)); margin-bottom: calc(var(--avatar-base-border-radius)); } +.avatars_inline .avatar:last-of-type { + margin-right: calc(var(--avatar-base-border-radius)); +} .group_select_block_list { display: flex; From fb97f563b7f817a4fa313cd5fa4bfad85e2229e6 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 7 Mar 2024 02:15:41 +0100 Subject: [PATCH 10/24] Tag Folders: show hidden numbers - Show hidden counts for folders, and total - Rework resizing of character names to shrink additional data first - Better placement for group numbers --- public/index.html | 7 +++---- public/script.js | 32 +++++++++++++++++++++++++--- public/scripts/group-chats.js | 5 +---- public/scripts/tags.js | 26 ++++++++++++++--------- public/style.css | 39 ++++++++++++++++++++++++++++++----- 5 files changed, 83 insertions(+), 26 deletions(-) diff --git a/public/index.html b/public/index.html index fa18f0764..79ae4540b 100644 --- a/public/index.html +++ b/public/index.html @@ -5000,10 +5000,9 @@
- group of - 5 - characters +
+ in this group
@@ -5020,8 +5019,8 @@
- characters
+
diff --git a/public/script.js b/public/script.js index 51cd8f5a9..07c823bb6 100644 --- a/public/script.js +++ b/public/script.js @@ -1175,7 +1175,7 @@ function getEmptyBlock() { const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it']; const roll = new Date().getMinutes() % icons.length; const emptyBlock = ` -
+

${texts[roll]}

There are no items to display.

@@ -1183,6 +1183,20 @@ function getEmptyBlock() { return $(emptyBlock); } +/** + * @param {number} hidden Number of hidden characters + */ +function getHiddenBlock(hidden) { + const hiddenBlick = ` +
+ +

${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.

+
+
+
`; + return $(hiddenBlick); +} + function getCharacterBlock(item, id) { let this_avatar = default_avatar; if (item.avatar != 'none') { @@ -1261,19 +1275,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, i.entities ?? 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) { @@ -1327,11 +1350,14 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) { // 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 }); + 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; } } diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 86e364c78..7aad2bad1 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -542,11 +542,8 @@ export function getGroupBlock(group) { 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); + template.find('.group_select_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`); 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); diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 255455ddd..e42c6c0f1 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -86,9 +86,10 @@ let tag_map = {}; * @param {Object} param1 Optional parameters, explained below. * @param {Boolean} [param1.globalDisplayFilters] When enabled, applies the final filter for the global list. Icludes filtering out entities in closed/hidden folders and empty folders. * @param {Object} [param1.subForEntity] When given an entity, the list of entities gets filtered specifically for that one as a "sub list", filtering out other tags, elements not tagged for this and hidden elements. + * @param {Boolean} [param1.filterHidden] Optional switch with which filtering out hidden items (from closed folders) can be disabled. * @returns The filtered list of entities */ -function filterByTagState(entities, { globalDisplayFilters = false, subForEntity = undefined } = {}) { +function filterByTagState(entities, { globalDisplayFilters = false, subForEntity = undefined, filterHidden = true } = {}) { const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); entities = entities.filter(entity => { @@ -108,7 +109,7 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity 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))) { + if (filterHidden && entity.type !== 'tag' && closedFolders.some(f => entitiesFilter.isElementTagged(entity, f.id) && !filterData.selected.includes(f.id))) { return false; } @@ -123,13 +124,13 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity } if (subForEntity !== undefined && subForEntity.type === 'tag') { - entities = filterTagSubEntities(subForEntity.item, entities); + entities = filterTagSubEntities(subForEntity.item, entities, { filterHidden : filterHidden }); } return entities; } -function filterTagSubEntities(tag, entities) { +function filterTagSubEntities(tag, entities, { filterHidden = true } = {}) { 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); @@ -141,7 +142,7 @@ function filterTagSubEntities(tag, entities) { } // 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))) { + if (filterHidden && 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; } @@ -198,7 +199,14 @@ function chooseBogusFolder(source, tagId, remove = false) { } } -function getTagBlock(item, entities) { +/** + * Builds the tag block for the specified item. + * @param {Object} item The tag item + * @param {*} entities The list ob sub items for this tag + * @param {*} hidden A count of how many sub items are hidden + * @returns The html for the tag block + */ +function getTagBlock(item, entities, hidden = 0) { let count = entities.length; const tagFolder = TAG_FOLDER_TYPES[item.folder_type]; @@ -208,11 +216,9 @@ function getTagBlock(item, entities) { 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('.bogus_folder_counter').text(count); + template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : ''); + template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`); template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon); - if (count == 1) { - template.find('.character_unit_name').text('character'); - } // Fill inline character images buildAvatarList(template.find('.bogus_folder_avatars_block'), entities); diff --git a/public/style.css b/public/style.css index bf9cad51a..d152f6d97 100644 --- a/public/style.css +++ b/public/style.css @@ -1312,19 +1312,26 @@ input[type="file"] { align-self: center; } +#rm_print_characters_block .text_block { + height: 100%; + width: 100%; + opacity: 0.5; + margin: 0 auto 1px auto; + padding: 5px; +} + #rm_print_characters_block .empty_block { display: flex; flex-direction: column; gap: 10px; flex-wrap: wrap; text-align: center; - height: 100%; - width: 100%; - opacity: 0.5; justify-content: center; - margin: 0 auto; align-items: center; } +#rm_print_characters_block .hidden_block p { + display: inline; +} #rm_print_characters_block { overflow-y: auto; @@ -1626,6 +1633,15 @@ input[type=search]:focus::-webkit-search-cancel-button { align-items: baseline; flex-direction: row; gap: 5px; + margin-bottom: 6px; +} + +.character_name_block_sub_line { + position: absolute; + right: 0px; + top: calc(var(--mainFontSize) + 2px); + font-size: calc(var(--mainFontSize) * 0.6); + color: var(--grey7070a); } .ch_avatar_url { @@ -1651,15 +1667,28 @@ input[type=search]:focus::-webkit-search-cancel-button { } /*applies to both groups and solos chars in the char list*/ +#rm_print_characters_block .character_select_container, +#rm_print_characters_block .group_select_container { + position: relative; +} + #rm_print_characters_block .ch_name, .avatar-container .ch_name { - flex: 1; + flex: 1 1 auto; white-space: nowrap; overflow: hidden; + text-wrap: nowrap; text-overflow: ellipsis; display: block; } +#rm_print_characters_block .character_name_block > :last-child { + flex: 0 100000 auto; /* Force shrinking first */ + overflow: hidden; + text-wrap: nowrap; + text-overflow: ellipsis; +} + .bogus_folder_select:hover, .character_select:hover, .avatar-container:hover { From 9f42cafc39d8ce0295cb3b9159ff42cd1b3d65a2 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 7 Mar 2024 04:26:33 +0100 Subject: [PATCH 11/24] Tag Folders: Clear all filters button - Add "clear all filters" button for tag and search filters - Resize back button to take less space - Fix char grid display --- public/css/tags.css | 18 +++++++++ public/css/toggle-dependent.css | 10 +++-- public/index.html | 5 ++- public/scripts/filters.js | 2 +- public/scripts/tags.js | 70 +++++++++++++++++++++++++-------- 5 files changed, 83 insertions(+), 22 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 0c127d643..f99eb682b 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -68,6 +68,11 @@ align-items: center; } +.tag.actionable.clearAllFilters { + border: 0; + background: none; +} + .tagListHint { align-self: center; display: flex; @@ -217,6 +222,10 @@ opacity: 1; } +.rm_tag_bogus_drilldown { + height: calc(var(--mainFontSize)* 2 - 2); +} + .rm_tag_bogus_drilldown .tag:not(:first-child) { position: relative; margin-left: calc(var(--mainFontSize) * 2); @@ -238,3 +247,12 @@ opacity: 1; } +.bogus_folder_select_back .avatar { + display: none; +} + +.bogus_folder_select_back .bogus_folder_back_placeholder { + min-height: calc(var(--mainFontSize)*2); + width: var(--avatar-base-width); + justify-content: center; +} diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 72af383d2..fe4b0fc05 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -96,14 +96,18 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block { body.charListGrid #rm_print_characters_block .ch_description, body.charListGrid #rm_print_characters_block .tags_inline, body.charListGrid #rm_print_characters_block .character_version, -body.charListGrid #rm_print_characters_block .bogus_folder_unit, -body.charListGrid #rm_print_characters_block .group_select_unit, body.charListGrid #rm_print_characters_block .group_select_block_list, body.charListGrid #rm_print_characters_block .ch_avatar_url, -#user_avatar_block.gridView .avatar-container .ch_description { +body.charListGrid #rm_print_characters_block .character_name_block_sub_line, +#user_avatar_block.gridView .avatar-container .ch_description, +body.charListGrid #rm_print_characters_block .bogus_folder_select_back .bogus_folder_back_placeholder { display: none; } +body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar { + display: flex; +} + /*big avatars mode page-wide changes*/ body.big-avatars .character_select .avatar, diff --git a/public/index.html b/public/index.html index 79ae4540b..6d532d60e 100644 --- a/public/index.html +++ b/public/index.html @@ -5026,10 +5026,13 @@
-
+
+
+ +
Go back diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 1754d613d..d880cbf37 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -22,7 +22,7 @@ export const FILTER_TYPES = { export const FILTER_STATES = { SELECTED: { key: 'SELECTED', class: 'selected' }, EXCLUDED: { key: 'EXCLUDED', class: 'excluded' }, - UNDEFINED: { key: 'UNDEFINED', class: undefined }, + UNDEFINED: { key: 'UNDEFINED', class: 'undefined' }, }; /** diff --git a/public/scripts/tags.js b/public/scripts/tags.js index e42c6c0f1..16b6162d1 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -55,6 +55,7 @@ const ACTIONABLE_TAGS = { 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 = { @@ -490,7 +491,7 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe } if (tag.excluded && isGeneralList) { - toggleTagThreeState(tagElement, FILTER_STATES.EXCLUDED); + toggleTagThreeState(tagElement, { stateOverride: FILTER_STATES.EXCLUDED }); } if (selectable) { @@ -536,28 +537,44 @@ function onTagFilterClick(listElement) { updateTagFilterIndicator(); } -function toggleTagThreeState(element, stateOverride = undefined) { +function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) { const states = Object.keys(FILTER_STATES); - const overrideKey = states.includes(stateOverride) ? stateOverride : states.find(key => FILTER_STATES[key] === stateOverride); + const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride); - const currentState = element.attr('data-toggle-state') ?? states[states.length - 1]; - const nextState = overrideKey ?? states[(states.indexOf(currentState) + 1) % states.length]; + const currentStateIndex = states.indexOf(element.attr('data-toggle-state')) ?? states.length - 1; + const targetStateIndex = overrideKey !== undefined ? states.indexOf(overrideKey) : (currentStateIndex + 1) % states.length; - element.attr('data-toggle-state', nextState); - - console.debug('toggle three-way filter on', element, 'from', currentState, 'to', nextState); - - // Update css class and remove all others - Object.keys(FILTER_STATES).forEach(x => { - if (!isFilterState(x, FILTER_STATES.UNDEFINED)) { - element.toggleClass(FILTER_STATES[x].class, x === nextState); + if (simulateClick) { + // Calculate how many clicks are needed to go from the current state to the target state + let clickCount = 0; + if (targetStateIndex >= currentStateIndex) { + clickCount = targetStateIndex - currentStateIndex; + } else { + clickCount = (states.length - currentStateIndex) + targetStateIndex; } - }); - return nextState; + for (let i = 0; i < clickCount; i++) { + $(element).trigger('click'); + } + + console.debug('manually click-toggle three-way filter from', states[currentStateIndex], 'to', states[targetStateIndex], 'on', element); + } else { + element.attr('data-toggle-state', states[targetStateIndex]); + + // Update css class and remove all others + states.forEach(state => { + element.toggleClass(FILTER_STATES[state].class, state === states[targetStateIndex]); + }); + + console.debug('toggle three-way filter from', states[currentStateIndex], 'to', states[targetStateIndex], 'on', element); + } + + + return states[targetStateIndex]; } + function runTagFilters(listElement) { 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')))]; @@ -576,11 +593,13 @@ function printTagFilters(type = tag_filter_types.character) { .sort(compareTagsForSort); for (const tag of Object.values(ACTIONABLE_TAGS)) { + if (!power_user.bogus_folders && tag.id == ACTIONABLE_TAGS.FOLDER.id) { + continue; + } + 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 }); } @@ -1064,6 +1083,23 @@ function onTagListHintClick() { console.debug('show_tag_filters', power_user.show_tag_filters); } +function onClearAllFiltersClick() { + console.debug('clear all filters clicked'); + + // We have to manually go through the elements and unfilter by clicking... + // Thankfully nearly all filter controls are three-state-toggles + const filterTags = $('.rm_tag_controls .rm_tag_filter').find('.tag'); + for(const tag of filterTags) { + const toggleState = $(tag).attr('data-toggle-state'); + if (toggleState !== undefined && !isFilterState(toggleState ?? FILTER_STATES.UNDEFINED, FILTER_STATES.UNDEFINED)) { + toggleTagThreeState($(tag), { stateOverride: FILTER_STATES.UNDEFINED, simulateClick: true }); + } + } + + // Reset search too + $('#character_search_bar').val('').trigger('input'); +} + jQuery(() => { createTagInput('#tagInput', '#tagList'); createTagInput('#groupTagInput', '#groupTagList'); From 1b18969771a044b263a1d8d8bddbfeff69c94d87 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 7 Mar 2024 04:34:12 +0100 Subject: [PATCH 12/24] Tag Folders: Fix big avatars display --- public/css/tags.css | 2 +- public/css/toggle-dependent.css | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index f99eb682b..eeeae2bff 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -248,7 +248,7 @@ } .bogus_folder_select_back .avatar { - display: none; + display: none !important; } .bogus_folder_select_back .bogus_folder_back_placeholder { diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index fe4b0fc05..4d07c618d 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -105,7 +105,7 @@ body.charListGrid #rm_print_characters_block .bogus_folder_select_back .bogus_fo } body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar { - display: flex; + display: flex !important; } /*big avatars mode page-wide changes*/ @@ -116,7 +116,7 @@ body.big-avatars .bogus_folder_select .avatar { } body.big-avatars .avatar { - width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); /* width: unset; */ border-style: none; @@ -131,18 +131,18 @@ body.big-avatars .avatar { body.big-avatars #user_avatar_block .avatar, body.big-avatars #user_avatar_block .avatar_upload { - width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)); } body.big-avatars #user_avatar_block .avatar img { - width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); } body.big-avatars .avatar img { - width: calc(var(--avatar-base-height) * var(--big-avatar-width-factor)); + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor)); object-fit: cover; object-position: center; @@ -150,6 +150,10 @@ body.big-avatars .avatar img { border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor)); } +body.big-avatars .bogus_folder_select_back .bogus_folder_back_placeholder { + width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor)); +} + body:not(.big-avatars) .avatar_collage { min-width: var(--avatar-base-width); aspect-ratio: 1 / 1; From dae90373e75bef06ebcc9d8ed08a202e7f7980e6 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 7 Mar 2024 05:20:43 +0100 Subject: [PATCH 13/24] Tag Filters: Improve grid view - Add character count to groups and folders in grid view - Fix name cut-off for groups --- public/css/toggle-dependent.css | 26 +++++++++++++++++++++++--- public/index.html | 9 +++++---- public/style.css | 4 ++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 4d07c618d..b5add2bde 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -41,6 +41,7 @@ body.charListGrid #rm_print_characters_block { body.charListGrid #rm_print_characters_block .bogus_folder_select, body.charListGrid #rm_print_characters_block .character_select, +body.charListGrid #rm_print_characters_block .group_select, #user_avatar_block.gridView .avatar-container { width: 30%; align-items: flex-start; @@ -50,10 +51,19 @@ body.charListGrid #rm_print_characters_block .character_select, max-width: 100px; } +body.charListGrid #rm_print_characters_block .character_name_block { + gap: 0; /* Save a bit of space here */ + margin-bottom: 0; +} + body.charListGrid #rm_print_characters_block .bogus_folder_select .ch_name, +body.charListGrid #rm_print_characters_block .bogus_folder_select .bogus_folder_counter, body.charListGrid #rm_print_characters_block .character_select .ch_name, body.charListGrid #rm_print_characters_block .group_select .ch_name, -#user_avatar_block.gridView .avatar-container .ch_name { +body.charListGrid #rm_print_characters_block .group_select .group_select_counter, +#user_avatar_block.gridView .avatar-container .ch_name, +#user_avatar_block.gridView .avatar-container .bogus_folder_counter, +#user_avatar_block.gridView .avatar-container .group_select_counter { width: 100%; max-width: 100px; text-align: center; @@ -62,6 +72,7 @@ body.charListGrid #rm_print_characters_block .group_select .ch_name, body.charListGrid #rm_print_characters_block .bogus_folder_select .character_name_block, body.charListGrid #rm_print_characters_block .character_select .character_name_block, +body.charListGrid #rm_print_characters_block .group_select .group_name_block, #user_avatar_block.gridView .avatar-container .character_name_block { width: 100%; flex-direction: column; @@ -74,7 +85,9 @@ body.charListGrid #rm_print_characters_block .character_select .character_name_b body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container, body.charListGrid #rm_print_characters_block .character_select .character_select_container, -#user_avatar_block.gridView .avatar-container .character_select_container { +body.charListGrid #rm_print_characters_block .group_select .group_select_container, +#user_avatar_block.gridView .avatar-container .character_select_container, +#user_avatar_block.gridView .avatar-container .group_select_container { width: 100%; justify-content: center; max-width: 100px; @@ -95,9 +108,9 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block { body.charListGrid #rm_print_characters_block .ch_description, body.charListGrid #rm_print_characters_block .tags_inline, -body.charListGrid #rm_print_characters_block .character_version, body.charListGrid #rm_print_characters_block .group_select_block_list, body.charListGrid #rm_print_characters_block .ch_avatar_url, +body.charListGrid #rm_print_characters_block .character_version, body.charListGrid #rm_print_characters_block .character_name_block_sub_line, #user_avatar_block.gridView .avatar-container .ch_description, body.charListGrid #rm_print_characters_block .bogus_folder_select_back .bogus_folder_back_placeholder { @@ -108,9 +121,16 @@ body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar { display: flex !important; } + +body.charListGrid #rm_print_characters_block .ch_add_placeholder { + display: flex !important; + opacity: 0; /* Hack for keeping the spacing */ +} + /*big avatars mode page-wide changes*/ body.big-avatars .character_select .avatar, +body.big-avatars .group_select .avatar, body.big-avatars .bogus_folder_select .avatar { flex: unset; } diff --git a/public/index.html b/public/index.html index 6d532d60e..92ba990bc 100644 --- a/public/index.html +++ b/public/index.html @@ -4749,8 +4749,9 @@
- - + +++ + +
@@ -5000,7 +5001,7 @@
- +
in this group @@ -5018,7 +5019,7 @@
- +
diff --git a/public/style.css b/public/style.css index d152f6d97..002cf8ecd 100644 --- a/public/style.css +++ b/public/style.css @@ -1644,6 +1644,10 @@ input[type=search]:focus::-webkit-search-cancel-button { color: var(--grey7070a); } +.character_name_block .ch_add_placeholder { + display: none; +} + .ch_avatar_url { font-style: italic; } From 5ac7826fec59179c21a5c4ac275c78ee4bc1b587 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Thu, 7 Mar 2024 23:48:50 +0100 Subject: [PATCH 14/24] Tag Folders: Additional Fixes - Refactored to one general printTagList method - Made a robust getTagKeyForEntity function - Fixed group not displaying tags if autoloaded - Fixed added tags on character/groups not being auto sorted - Fixed autoload of group/character that I killed - Fixed typo --- public/script.js | 22 ++-- public/scripts/BulkEditOverlay.js | 4 +- public/scripts/RossAscends-mods.js | 11 +- public/scripts/group-chats.js | 13 +-- public/scripts/tags.js | 159 +++++++++++++++++++---------- public/scripts/world-info.js | 4 +- 6 files changed, 131 insertions(+), 82 deletions(-) diff --git a/public/script.js b/public/script.js index 350b19e42..6be19fd49 100644 --- a/public/script.js +++ b/public/script.js @@ -163,8 +163,8 @@ import { getTagBlock, loadTagsSettings, printTagFilters, - getTagsList, - appendTagToList, + getTagKeyForEntity, + printTagList, createTagMapFromList, renameTagKey, importTags, @@ -803,8 +803,11 @@ let token; var PromptArrayItemForRawPromptDisplay; +/** The tag of the active character. (NOT the id) */ export let active_character = ''; +/** The tag of the active group. (Coincidentally also the id) */ export let active_group = ''; + export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100)); export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100)); @@ -877,12 +880,12 @@ export function setAnimationDuration(ms = null) { animation_duration = ms ?? ANIMATION_DURATION_DEFAULT; } -export function setActiveCharacter(character) { - active_character = character; +export function setActiveCharacter(entityOrKey) { + active_character = getTagKeyForEntity(entityOrKey); } -export function setActiveGroup(group) { - active_group = group; +export function setActiveGroup(entityOrKey) { + active_group = getTagKeyForEntity(entityOrKey); } /** @@ -1187,14 +1190,14 @@ function getEmptyBlock() { * @param {number} hidden Number of hidden characters */ function getHiddenBlock(hidden) { - const hiddenBlick = ` + const hiddenBlock = `

${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.

`; - return $(hiddenBlick); + return $(hiddenBlock); } function getCharacterBlock(item, id) { @@ -1233,9 +1236,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; 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 8c7d34c8c..3cadf8978 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -11,7 +11,6 @@ import { setActiveGroup, setActiveCharacter, getEntitiesList, - getThumbnailUrl, buildAvatarList, selectCharacterById, eventSource, @@ -27,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, @@ -248,8 +248,7 @@ 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); - + const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) == active_character); if (active_character_id !== null) { await selectCharacterById(String(active_character_id)); } @@ -805,14 +804,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('grid') || $(this).data('id'); setActiveCharacter(null); setActiveGroup(groupId); saveSettingsDebounced(); diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 7aad2bad1..1cdfb8cfc 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 { @@ -546,9 +546,8 @@ export function getGroupBlock(group) { template.find('.group_select_block_list').append(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) { @@ -579,7 +578,7 @@ function isValidImageUrl(url) { function getGroupAvatar(group) { if (!group) { - return $(`
`); + return $(`
`); } // if isDataURL or if it's a valid local file url if (isValidImageUrl(group.avatar_url)) { @@ -1185,9 +1184,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(); @@ -1263,6 +1261,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(); diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 16b6162d1..d89d7353c 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -29,6 +29,7 @@ export { loadTagsSettings, printTagFilters, getTagsList, + printTagList, appendTagToList, createTagMapFromList, renameTagKey, @@ -308,12 +309,37 @@ function getTagKey() { return null; } -export function getTagKeyForCharacter(characterId = null) { - return characters[characterId]?.avatar; +/** + * Gets the tag key for any provided entity/id/key. If a valid tag key is provided, it just returns this. + * Robust method to find a valid tag key for any entity + * @param {object|number|string} entityOrKey An entity with id property (character, group, tag), or directly an id or tag key. + * @returns {string} The tag key that can be found. + */ +export function getTagKeyForEntity(entityOrKey) { + let x = entityOrKey; + + // If it's an object and has an 'id' property, we take this for further processing + if (typeof x === 'object' && x !== null && 'id' in x) { + x = x.id; + } + + // Next lets check if its a valid character or character id, so we can swith it to its tag + const character = characters.indexOf(x) > 0 ? x : characters[x]; + if (character) { + x = character.avatar; + } + + // We should hopefully have a key now. Let's check + if (x in tag_map) { + return x; + } + + // If none of the above, we cannot find a valid tag key + return undefined; } function addTagToMap(tagId, characterId = null) { - const key = getTagKey() ?? getTagKeyForCharacter(characterId); + const key = getTagKey() ?? getTagKeyForEntity(characterId); if (!key) { return; @@ -329,7 +355,7 @@ function addTagToMap(tagId, characterId = null) { } function removeTagFromMap(tagId, characterId = null) { - const key = getTagKey() ?? getTagKeyForCharacter(characterId); + const key = getTagKey() ?? getTagKeyForEntity(characterId); if (!key) { return; @@ -370,10 +396,6 @@ function selectTag(event, ui, listSelector) { // unfocus and clear the input $(event.target).val('').trigger('input'); - // add tag to the UI and internal map - appendTagToList(listSelector, tag, { removable: true }); - appendTagToList(getInlineListSelector(), tag, { removable: false }); - // 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; @@ -385,6 +407,11 @@ function selectTag(event, ui, listSelector) { } saveSettingsDebounced(); + + // add tag to the UI and internal map - we reprint so sorting and new markup is done correctly + printTagList(listSelector, { tagOptions: { removable: true } }); + printTagList($(getInlineListSelector())); + printTagFilters(tag_filter_types.character); printTagFilters(tag_filter_types.group_member); @@ -458,18 +485,63 @@ function createNewTag(tagName) { return tag; } +/** + * @typedef {object} TagOptions + * @property {boolean} [removable=false] - Whether tags can be removed. + * @property {boolean} [selectable=false] - Whether tags can be selected. + * @property {function} [action=undefined] - Action to perform on tag interaction. + * @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags. + * @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists. + */ + +/** + * Prints the list of tags. + * @param {JQuery} element - The container element where the tags are to be printed. + * @param {object} [options] - Optional parameters for printing the tag list. + * @param {Array} [options.tags] Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. + * @param {object|number|string} [options.forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. + * @param {boolean} [options.empty=true] - Whether the list should be initially empty. + * @param {function(object): function} [options.tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions. + * If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself. + * @param {TagOptions} [options.tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList") + */ +function printTagList(element, { tags = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) { + const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey(); + const printableTags = tags ?? getTagsList(key); + + if (empty) { + $(element).empty(); + } + + for (const tag of printableTags) { + // If we have a custom action selector, we override that tag options for each tag + if (tagActionSelector && typeof tagActionSelector === 'function') { + const action = tagActionSelector(tag); + if (action && typeof action !== 'function') { + console.error('The action parameter must return a function for tag.', tag); + } else { + tagOptions.action = action; + } + } + + appendTagToList(element, tag, tagOptions); + } +} + /** * 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 + * @param {JQuery} listElement List element. + * @param {object} tag Tag object to append. + * @param {TagOptions} [options={}] - Options for tag behavior. * @returns {void} */ -function appendTagToList(listElement, tag, { removable, selectable, action, isGeneralList }) { +function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, isGeneralList = false, skipExistsCheck = false } = {}) { if (!listElement) { return; } + if (!skipExistsCheck && $(listElement).find(`.tag[id="${tag.id}"]`).length > 0) { + return; + } let tagElement = $('#tag_template .tag').clone(); tagElement.attr('id', tag.id); @@ -527,7 +599,7 @@ function onTagFilterClick(listElement) { 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 }); + appendTagToList($('.rm_tag_controls .rm_tag_bogus_drilldown'), existingTag, { removable: true }); } else { $(listElement).closest('.rm_tag_controls').find(`.rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove(); } @@ -574,7 +646,6 @@ function toggleTagThreeState(element, { stateOverride = undefined, simulateClick return states[targetStateIndex]; } - function runTagFilters(listElement) { 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')))]; @@ -583,35 +654,29 @@ function runTagFilters(listElement) { } function printTagFilters(type = tag_filter_types.character) { + const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR; - const selectedTagIds = [...($(FILTER_SELECTOR).find('.tag.selected').map((_, el) => $(el).attr('id')))]; $(FILTER_SELECTOR).empty(); $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown').empty(); + + // Print all action tags. (Exclude folder if that setting isn't chosen) + const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id); + printTagList($(FILTER_SELECTOR), { empty: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } }); + + const inListActionTags = Object.values(InListActionable); + printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } }); + const characterTagIds = Object.values(tag_map).flat(); const tagsToDisplay = tags .filter(x => characterTagIds.includes(x.id)) .sort(compareTagsForSort); + printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } }); - for (const tag of Object.values(ACTIONABLE_TAGS)) { - if (!power_user.bogus_folders && tag.id == ACTIONABLE_TAGS.FOLDER.id) { - continue; - } + runTagFilters(FILTER_SELECTOR); - appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true }); - } - - 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); - } - } - - for (const tagId of selectedTagIds) { - $(`${FILTER_SELECTOR} .tag[id="${tagId}"]`).trigger('click'); + // Simulate clicks on all "selected" tags when we reprint, otherwise their filter gets lost. "excluded" is persisted. + for (const tagId of filterData.selected) { + toggleTagThreeState($(`${FILTER_SELECTOR} .tag[id="${tagId}"]`), { stateOverride: FILTER_STATES.SELECTED, simulateClick: true }); } if (power_user.show_tag_filters) { @@ -679,36 +744,18 @@ function onCharacterCreateClick() { } function onGroupCreateClick() { - $('#groupTagList').empty(); - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); + // Nothing to do here at the moment. Tags in group interface get automatically redrawn. } export function applyTagsOnCharacterSelect() { //clearTagsFilter(); const chid = Number($(this).attr('chid')); - const key = characters[chid].avatar; - const tags = getTagsList(key); - - $('#tagList').empty(); - - for (const tag of tags) { - appendTagToList('#tagList', tag, { removable: true }); - } + printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } }); } function applyTagsOnGroupSelect() { //clearTagsFilter(); - const key = $(this).attr('grid'); - const tags = getTagsList(key); - - $('#groupTagList').empty(); - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); - - for (const tag of tags) { - appendTagToList('#groupTagList', tag, { removable: true }); - } + // Nothing to do here at the moment. Tags in group interface get automatically redrawn. } export function createTagInput(inputSelector, listSelector) { diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 5352fce5a..7774c7a3f 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -7,7 +7,7 @@ import { isMobile } from './RossAscends-mods.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; import { getTokenCount } from './tokenizers.js'; import { power_user } from './power-user.js'; -import { getTagKeyForCharacter } from './tags.js'; +import { getTagKeyForEntity } from './tags.js'; import { resolveVariable } from './variables.js'; export { @@ -2068,7 +2068,7 @@ async function checkWorldInfo(chat, maxContext) { } if (entry.characterFilter && entry.characterFilter?.tags?.length > 0) { - const tagKey = getTagKeyForCharacter(this_chid); + const tagKey = getTagKeyForEntity(this_chid); if (tagKey) { const tagMapEntry = context.tagMap[tagKey]; From 73fdcbad440f745dd995699fcbb67bef5ff522a6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 9 Mar 2024 21:58:13 +0200 Subject: [PATCH 15/24] Fix some bugs (see below): 1. Remove yellow highlights in hotswaps list. 2. Decrease font size of group members list, make it respect the block limits (1 row for small avatars, 3 rows for big avatars). 3. Fix autoload loading the first character if the latest selected entity was a group. 4. Fix tag key potentially skipping the first character. 5. Fix being unable to open groups from the hotswaps panel. 6. Fix left alignment of hotswaps panel, now centered. 7. Fix rounding of missing group avatars (most noticeable when favorited). --- public/index.html | 2 +- public/script.js | 8 ++++--- public/scripts/RossAscends-mods.js | 14 +++++++----- public/scripts/group-chats.js | 10 +++++---- public/scripts/tags.js | 2 +- public/style.css | 35 ++++++++++-------------------- 6 files changed, 33 insertions(+), 38 deletions(-) diff --git a/public/index.html b/public/index.html index 17aed9bbf..c619c3b07 100644 --- a/public/index.html +++ b/public/index.html @@ -5030,7 +5030,7 @@ in this group -
+
diff --git a/public/script.js b/public/script.js index 7977bbe7c..9ec3478e6 100644 --- a/public/script.js +++ b/public/script.js @@ -5298,7 +5298,7 @@ function getThumbnailUrl(type, file) { return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; } -function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false } = {}) { +function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) { if (empty) { block.empty(); } @@ -5318,8 +5318,10 @@ function buildAvatarList(block, entities, { templateId = 'inline_avatar_template 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}`); - avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true'); - avatarTemplate.find('.ch_fav').val(entity.item.fav); + 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') { diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 46d892c01..5b11eb43d 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -248,12 +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 - const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) == 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)); } @@ -273,7 +275,7 @@ export async function favsToHotswap() { return; } - buildAvatarList(container, favs, { selectable: true }); + buildAvatarList(container, favs, { selectable: true, highlightFavs: false }); } //changes input bar and send button display depending on connection status @@ -812,7 +814,7 @@ export function initRossMods() { }); $(document).on('click', '.group_select', function () { - const groupId = $(this).attr('grid') || $(this).data('id'); + const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id'); setActiveCharacter(null); setActiveGroup(groupId); saveSettingsDebounced(); diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 1cdfb8cfc..e8a7e8071 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -529,9 +529,11 @@ export function getGroupBlock(group) { // Build inline name list if (Array.isArray(group.members) && group.members.length) { for (const member of group.members) { - count++; const character = characters.find(x => x.avatar === member || x.name === member); - namesList.push(`${character.name}`); + if (character) { + namesList.push(character.name); + count++; + } } } @@ -543,7 +545,7 @@ export function getGroupBlock(group) { 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').append(namesList.join('')); + template.find('.group_select_block_list').text(namesList.join(', ')); // Display inline tags const tagsElement = template.find('.tags'); @@ -1761,7 +1763,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/tags.js b/public/scripts/tags.js index d89d7353c..7b798f828 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -324,7 +324,7 @@ export function getTagKeyForEntity(entityOrKey) { } // Next lets check if its a valid character or character id, so we can swith it to its tag - const character = characters.indexOf(x) > 0 ? x : characters[x]; + const character = characters.indexOf(x) >= 0 ? x : characters[x]; if (character) { x = character.avatar; } diff --git a/public/style.css b/public/style.css index 002cf8ecd..604441b0d 100644 --- a/public/style.css +++ b/public/style.css @@ -884,6 +884,11 @@ hr { .hotswap { margin: 5px; + justify-content: space-evenly; +} + +#HotSwapWrapper { + justify-content: space-evenly; } .avatar.selectable { @@ -955,30 +960,11 @@ hr { margin-left: calc(var(--avatar-base-border-radius)); margin-bottom: calc(var(--avatar-base-border-radius)); } + .avatars_inline .avatar:last-of-type { margin-right: calc(var(--avatar-base-border-radius)); } -.group_select_block_list { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: flex-start; - gap: 0.2rem; - align-items: flex-end; - flex-basis: 100%; -} - -.group_select_block_list .group_ch_name:not(:last-child):not(:nth-last-child(2)):after { - content: ", "; - font-size: calc(var(--mainFontSize) * 0.9); -} - -.group_select_block_list .group_ch_name:nth-last-child(2):after { - content: " & "; - font-size: calc(var(--mainFontSize) * 0.9); -} - .mes_block { padding-top: 0; padding-left: 10px; @@ -1329,6 +1315,7 @@ input[type="file"] { justify-content: center; align-items: center; } + #rm_print_characters_block .hidden_block p { display: inline; } @@ -1617,7 +1604,8 @@ input[type=search]:focus::-webkit-search-cancel-button { margin-bottom: 1px; } -.character_select.inline_avatar { +.character_select.inline_avatar, +.missing-avatar.inline_avatar { padding: unset; border-radius: var(--avatar-base-border-radius-round); } @@ -1686,8 +1674,9 @@ input[type=search]:focus::-webkit-search-cancel-button { display: block; } -#rm_print_characters_block .character_name_block > :last-child { - flex: 0 100000 auto; /* Force shrinking first */ +#rm_print_characters_block .character_name_block> :last-child { + flex: 0 100000 auto; + /* Force shrinking first */ overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis; From 9aadc7c32b6a4997582ee76bcb9a2d84f7a327a4 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Tue, 12 Mar 2024 23:39:54 +0100 Subject: [PATCH 16/24] Tag Folders: Fix tag sortable being stuck while drawing - When drag&dropping tags, the tag was stuck until the character list was stuck. Make it a debounced call now. --- public/scripts/tags.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 7b798f828..3843733a4 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -13,7 +13,7 @@ import { import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js'; import { groupCandidatesFilter, groups, selected_group } from './group-chats.js'; -import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js'; +import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js'; import { power_user } from './power-user.js'; export { @@ -847,10 +847,12 @@ function makeTagListDraggable(tagContainer) { 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); + // If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags. + debounce(() => { + printCharacters(false); + printTagFilters(tag_filter_types.character); + printTagFilters(tag_filter_types.group_member); + }, 100); }; // @ts-ignore From 81503b7bd51ea2090f08842ba5a844fed6c8fb0e Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 13 Mar 2024 02:18:25 +0100 Subject: [PATCH 17/24] Tag Filters: Fix the debounce redrawing again --- public/scripts/tags.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 3843733a4..2615d87b3 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -45,6 +45,12 @@ 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, @@ -848,11 +854,7 @@ function makeTagListDraggable(tagContainer) { saveSettingsDebounced(); // If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags. - debounce(() => { - printCharacters(false); - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); - }, 100); + redrawCharsAndFiltersDebounced(); }; // @ts-ignore From cc23169374b170dba74fb578cb8c88c5a00bd051 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 22:37:01 +0200 Subject: [PATCH 18/24] Add a gap to tag controls row --- public/css/tags.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/css/tags.css b/public/css/tags.css index 94e841a76..d440a4851 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -128,6 +128,7 @@ .rm_tag_controls { display: flex; column-gap: 10px; + row-gap: 5px; flex-direction: row; flex-wrap: wrap; align-items: flex-start; From d1a8a4478ba287a115281cc6e570440c50cca01e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:01:47 +0200 Subject: [PATCH 19/24] Improve alignment of missing avatars --- public/style.css | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/public/style.css b/public/style.css index cc1f6a067..e07fb2033 100644 --- a/public/style.css +++ b/public/style.css @@ -3046,8 +3046,27 @@ body .ui-widget-content li:hover { width: 0px; } +#group_avatar_preview .missing-avatar { + display: inline; + vertical-align: middle; +} + +body.big-avatars .group_select .missing-avatar { + display: flex; + justify-content: center; + align-items: center; +} + +body.big-avatars .missing-avatar { + width: calc(var(--avatar-base-width)* var(--big-avatar-width-factor)); + height: calc(var(--avatar-base-height)* var(--big-avatar-height-factor)); +} + .missing-avatar { font-size: 36px; + width: var(--avatar-base-width); + height: var(--avatar-base-height); + text-align: center; } @keyframes ellipsis { From 88075bde79de8c853f8209f18dd205d33465bb89 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:05:16 +0200 Subject: [PATCH 20/24] Update hotswap avatars when changing group members list --- public/scripts/group-chats.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 0724d1f02..57da890ee 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -574,6 +574,8 @@ function updateGroupAvatar(group) { $(this).find('.avatar').replaceWith(getGroupAvatar(group)); } }); + + favsToHotswap(); } // check if isDataURLor if it's a valid local file url From 0b264bc51978ba8db1ab71b33c1c7f79d87de0a4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:11:07 +0200 Subject: [PATCH 21/24] Fix cursor of hidden block --- public/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/style.css b/public/style.css index e07fb2033..82ba18066 100644 --- a/public/style.css +++ b/public/style.css @@ -1322,6 +1322,10 @@ input[type="file"] { align-items: center; } +.hidden_block { + cursor: default; +} + #rm_print_characters_block .hidden_block p { display: inline; } From a789c6f76f36bc7e5c391542706e1002806ed67b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:20:35 +0200 Subject: [PATCH 22/24] Format fix --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 3c2659d0e..674d6296e 100644 --- a/public/script.js +++ b/public/script.js @@ -1357,7 +1357,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) { } // Run over all entities between first and second filter to save some states - for(const entity of entities) { + 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') { From ad450981c0b213199390597f3ed8238cfee71c79 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:35:41 +0200 Subject: [PATCH 23/24] Fix unfocusing tag input when switching characters --- public/script.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/script.js b/public/script.js index 674d6296e..56d62bbc6 100644 --- a/public/script.js +++ b/public/script.js @@ -5351,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(); From 80a207b696a59445091c45e7497b25c5782a1d75 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:36:37 +0200 Subject: [PATCH 24/24] Format CSS comments --- public/css/tags.css | 1 + public/css/toggle-dependent.css | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index d440a4851..b919b8300 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -185,6 +185,7 @@ .tag_as_folder { filter: brightness(75%) saturate(0.6); } + .tag_as_folder:hover { filter: brightness(150%) saturate(0.6) !important; } diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index b5add2bde..13067d50a 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -51,8 +51,9 @@ body.charListGrid #rm_print_characters_block .group_select, max-width: 100px; } +/* Save a bit of space here */ body.charListGrid #rm_print_characters_block .character_name_block { - gap: 0; /* Save a bit of space here */ + gap: 0; margin-bottom: 0; } @@ -121,10 +122,10 @@ body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar { display: flex !important; } - +/* Hack for keeping the spacing */ body.charListGrid #rm_print_characters_block .ch_add_placeholder { display: flex !important; - opacity: 0; /* Hack for keeping the spacing */ + opacity: 0; } /*big avatars mode page-wide changes*/