mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-09 08:38:53 +01:00
Merge pull request #1901 from Wolfsblvt/tags-as-folders-enhancements
Tag Folders Enhancements : folder sorting, folder types, navigation, inline display
This commit is contained in:
commit
870fdd93d2
@ -68,6 +68,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag.actionable.clearAllFilters {
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tagListHint {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
@ -123,7 +128,9 @@
|
||||
.rm_tag_controls {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
row-gap: 5px;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
margin: 5px;
|
||||
}
|
||||
@ -157,13 +164,13 @@
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.tag.excluded:after {
|
||||
.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);
|
||||
@ -174,3 +181,80 @@
|
||||
1px -1px 0px black;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tag_as_folder {
|
||||
filter: brightness(75%) saturate(0.6);
|
||||
}
|
||||
|
||||
.tag_as_folder:hover {
|
||||
filter: brightness(150%) saturate(0.6) !important;
|
||||
}
|
||||
|
||||
.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);
|
||||
font-size: calc(var(--mainFontSize) * 1);
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.bogus_folder_select_back .avatar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.bogus_folder_select_back .bogus_folder_back_placeholder {
|
||||
min-height: calc(var(--mainFontSize)*2);
|
||||
width: var(--avatar-base-width);
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
@ -18,10 +24,8 @@ body.no-modelIcons .icon-svg {
|
||||
}
|
||||
|
||||
body.square-avatars .avatar,
|
||||
body.square-avatars .avatar img,
|
||||
body.square-avatars .hotswapAvatar,
|
||||
body.square-avatars .hotswapAvatar img {
|
||||
border-radius: 2px !important;
|
||||
body.square-avatars .avatar img {
|
||||
border-radius: var(--avatar-base-border-radius) !important;
|
||||
}
|
||||
|
||||
/*char list grid mode*/
|
||||
@ -37,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;
|
||||
@ -46,10 +51,20 @@ body.charListGrid #rm_print_characters_block .character_select,
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
/* Save a bit of space here */
|
||||
body.charListGrid #rm_print_characters_block .character_name_block {
|
||||
gap: 0;
|
||||
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;
|
||||
@ -58,6 +73,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;
|
||||
@ -70,7 +86,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;
|
||||
@ -89,29 +107,38 @@ 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 .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_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 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/*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;
|
||||
}
|
||||
|
||||
body:not(.big-avatars) .avatar {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
body.big-avatars .avatar {
|
||||
width: 60px;
|
||||
height: 90px;
|
||||
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;
|
||||
display: flex;
|
||||
@ -120,33 +147,36 @@ body.big-avatars .avatar {
|
||||
align-items: center;
|
||||
/* align-self: unset; */
|
||||
overflow: visible;
|
||||
border-radius: 10px;
|
||||
flex: 1
|
||||
border-radius: calc(var(--avatar-base-border-radius) * var(--big-avatar-border-factor));
|
||||
}
|
||||
|
||||
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-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 {
|
||||
height: 90px;
|
||||
width: 60px;
|
||||
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: 60px;
|
||||
height: 90px;
|
||||
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;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
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: 50px;
|
||||
min-width: var(--avatar-base-width);
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
@ -155,8 +185,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 +199,65 @@ body.big-avatars .avatar-container .ch_description {
|
||||
text-overflow: unset;
|
||||
}
|
||||
|
||||
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 .avatars_inline {
|
||||
max-height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) + 2 * var(--avatar-base-border-radius));
|
||||
}
|
||||
|
||||
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_small .avatar_collage {
|
||||
min-width: calc(var(--avatar-base-width) * var(--inline-avatar-small-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 */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -3939,7 +3939,7 @@
|
||||
<div class="right_menu_button fa-solid fa-list-ul" id="rm_button_characters" title="Select/Create Characters" data-i18n="[title]Select/Create Characters"></div>
|
||||
</div>
|
||||
<div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto width100p">
|
||||
<div class="hotswap flex-container flex1"></div>
|
||||
<div class="hotswap avatars_inline flex-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@ -4249,6 +4249,7 @@
|
||||
</form>
|
||||
<div class="rm_tag_controls">
|
||||
<div class="tags rm_tag_filter"></div>
|
||||
<div class="tags rm_tag_bogus_drilldown"></div>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
@ -4581,6 +4582,10 @@
|
||||
</div>
|
||||
<div id="tag_view_template" class="template_element">
|
||||
<div class="tag_view_item">
|
||||
<div class="drag-handle" data-i18n="[title]Drag to reorder tag">☰</div>
|
||||
<div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder">
|
||||
<span class="tag_folder_indicator"></span>
|
||||
</div>
|
||||
<div class="tagColorPickerHolder"></div>
|
||||
<div class="tagColorPicker2Holder"></div>
|
||||
<div class="tag_view_name" contenteditable="true"></div>
|
||||
@ -4794,8 +4799,9 @@
|
||||
<div class="flex-container wide100pLess70px character_select_container">
|
||||
<div class="wide100p character_name_block">
|
||||
<span class="ch_name"></span>
|
||||
<small class="character_version"></small>
|
||||
<small class="ch_avatar_url"></small>
|
||||
<small class="ch_additional_info ch_add_placeholder">+++</small>
|
||||
<small class="ch_additional_info character_version"></small>
|
||||
<small class="ch_additional_info ch_avatar_url"></small>
|
||||
</div>
|
||||
<i class="ch_fav_icon fa-solid fa-star"></i>
|
||||
<input class="ch_fav" value="" hidden />
|
||||
@ -5042,10 +5048,15 @@
|
||||
<div class="avatar">
|
||||
<img src="">
|
||||
</div>
|
||||
<div class="flex-container wide100pLess70px gap5px group_name_block">
|
||||
<div class="ch_name"></div>
|
||||
<div class="flex-container wide100pLess70px gap5px group_select_container">
|
||||
<div class="wide100p group_name_block character_name_block">
|
||||
<div class="ch_name"></div>
|
||||
<small class="ch_additional_info group_select_counter"></small>
|
||||
</div>
|
||||
<small class="character_name_block_sub_line">in this group</small>
|
||||
<i class='group_fav_icon fa-solid fa-star'></i>
|
||||
<input class="ch_fav" value="" hidden />
|
||||
<div class="group_select_block_list ch_description"></div>
|
||||
<div class="tags tags_inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -5053,24 +5064,26 @@
|
||||
<div id="bogus_folder_template" class="template_element">
|
||||
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart">
|
||||
<div class="avatar flex alignitemscenter textAlignCenter">
|
||||
<i class="bogus_folder_icon fa-solid fa-folder-open fa-xl"></i>
|
||||
<i class="bogus_folder_icon fa-solid fa-xl"></i>
|
||||
</div>
|
||||
<div class="flex-container wide100pLess70px character_select_container">
|
||||
<div class="wide100p character_name_block">
|
||||
<span class="ch_name"></span>
|
||||
<small class="ch_additional_info bogus_folder_counter"></small>
|
||||
</div>
|
||||
<div class="bogus_folder_counter_block">
|
||||
<span class="bogus_folder_counter"></span>
|
||||
<span data="character card(s)">character card(s)</span>
|
||||
</div>
|
||||
<small class="character_name_block_sub_line bogus_folder_hidden_counter"></small>
|
||||
<div class="bogus_folder_avatars_block avatars_inline avatars_inline_small tags tags_inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bogus_folder_back_template" class="template_element">
|
||||
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart" id="BogusFolderBack" tagid="back">
|
||||
<div class="bogus_folder_select bogus_folder_select_back flex-container wide100p alignitemsflexstart" id="BogusFolderBack" tagid="back">
|
||||
<div class="avatar flex alignitemscenter textAlignCenter">
|
||||
<i class="bogus_folder_icon fa-solid fa-xl fa-right-from-bracket fa-flip-horizontal"></i>
|
||||
</div>
|
||||
<div class="bogus_folder_back_placeholder flex alignitemscenter textAlignCenter">
|
||||
<i class="bogus_folder_icon fa-solid fa-xl fa-right-from-bracket fa-flip-horizontal"></i>
|
||||
</div>
|
||||
<div class="flex-container wide100pLess70px character_select_container height100p alignitemscenter">
|
||||
<div class="wide100p character_name_block">
|
||||
<span class="ch_name">Go back</span>
|
||||
@ -5078,9 +5091,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hotswap_template" class="template_element">
|
||||
<div class="hotswapAvatar" title="Add a character/group to favorites to display it here!">
|
||||
<img src="/img/ai4.png">
|
||||
<div id="inline_avatar_template" class="template_element">
|
||||
<div class="avatar inline_avatar flex alignitemscenter textAlignCenter">
|
||||
<img src="">
|
||||
</div>
|
||||
</div>
|
||||
<div id="alternate_greetings_template" class="template_element">
|
||||
|
216
public/script.js
216
public/script.js
@ -156,14 +156,20 @@ import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, proce
|
||||
import {
|
||||
tag_map,
|
||||
tags,
|
||||
filterByTagState,
|
||||
isBogusFolder,
|
||||
isBogusFolderOpen,
|
||||
chooseBogusFolder,
|
||||
getTagBlock,
|
||||
loadTagsSettings,
|
||||
printTagFilters,
|
||||
getTagsList,
|
||||
appendTagToList,
|
||||
getTagKeyForEntity,
|
||||
printTagList,
|
||||
createTagMapFromList,
|
||||
renameTagKey,
|
||||
importTags,
|
||||
tag_filter_types,
|
||||
compareTagsForSort,
|
||||
} from './scripts/tags.js';
|
||||
import {
|
||||
SECRET_KEYS,
|
||||
@ -243,6 +249,7 @@ export {
|
||||
scrollChatToBottom,
|
||||
isStreamingEnabled,
|
||||
getThumbnailUrl,
|
||||
buildAvatarList,
|
||||
getStoppingStrings,
|
||||
reloadMarkdownProcessor,
|
||||
getCurrentChatId,
|
||||
@ -802,8 +809,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));
|
||||
|
||||
@ -876,12 +886,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1167,23 +1177,6 @@ export async function selectCharacterById(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTagBlock(item, entities) {
|
||||
let count = 0;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (entitiesFilter.isElementTagged(entity, item.id)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
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('.bogus_folder_counter').text(count);
|
||||
return template;
|
||||
}
|
||||
|
||||
function getBackBlock() {
|
||||
const template = $('#bogus_folder_back_template .bogus_folder_select').clone();
|
||||
return template;
|
||||
@ -1194,7 +1187,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 = `
|
||||
<div class="empty_block">
|
||||
<div class="text_block empty_block">
|
||||
<i class="fa-solid ${icons[roll]} fa-4x"></i>
|
||||
<h1>${texts[roll]}</h1>
|
||||
<p>There are no items to display.</p>
|
||||
@ -1202,6 +1195,20 @@ function getEmptyBlock() {
|
||||
return $(emptyBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} hidden Number of hidden characters
|
||||
*/
|
||||
function getHiddenBlock(hidden) {
|
||||
const hiddenBlock = `
|
||||
<div class="text_block hidden_block">
|
||||
<small>
|
||||
<p>${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.</p>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Characters and groups hidden by filters or closed folders" title="Characters and groups hidden by filters or closed folders"></div>
|
||||
</small>
|
||||
</div>`;
|
||||
return $(hiddenBlock);
|
||||
}
|
||||
|
||||
function getCharacterBlock(item, id) {
|
||||
let this_avatar = default_avatar;
|
||||
if (item.avatar != 'none') {
|
||||
@ -1210,9 +1217,9 @@ function getCharacterBlock(item, id) {
|
||||
// Populate the template
|
||||
const template = $('#character_template .character_select').clone();
|
||||
template.attr({ 'chid': id, 'id': `CharID${id}` });
|
||||
template.find('img').attr('src', this_avatar);
|
||||
template.find('.avatar').attr('title', item.avatar);
|
||||
template.find('.ch_name').text(item.name);
|
||||
template.find('img').attr('src', this_avatar).attr('alt', item.name);
|
||||
template.find('.avatar').attr('title', `[Character] ${item.name}`);
|
||||
template.find('.ch_name').text(item.name).attr('title', `[Character] ${item.name}`);
|
||||
if (power_user.show_card_avatar_urls) {
|
||||
template.find('.ch_avatar_url').text(item.avatar);
|
||||
}
|
||||
@ -1238,9 +1245,8 @@ function getCharacterBlock(item, id) {
|
||||
}
|
||||
|
||||
// Display inline tags
|
||||
const tags = getTagsList(item.avatar);
|
||||
const tagsElement = template.find('.tags');
|
||||
tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
|
||||
printTagList(tagsElement, { forEntityOrKey: id });
|
||||
|
||||
// Add to the list
|
||||
return template;
|
||||
@ -1252,11 +1258,6 @@ async function printCharacters(fullRefresh = false) {
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
|
||||
// Return to main list
|
||||
if (isBogusFolderOpen()) {
|
||||
entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [] });
|
||||
}
|
||||
|
||||
await delay(1);
|
||||
}
|
||||
|
||||
@ -1285,19 +1286,28 @@ async function printCharacters(fullRefresh = false) {
|
||||
if (!data.length) {
|
||||
$(listId).append(getEmptyBlock());
|
||||
}
|
||||
let displayCount = 0;
|
||||
for (const i of data) {
|
||||
switch (i.type) {
|
||||
case 'character':
|
||||
$(listId).append(getCharacterBlock(i.item, i.id));
|
||||
displayCount++;
|
||||
break;
|
||||
case 'group':
|
||||
$(listId).append(getGroupBlock(i.item));
|
||||
displayCount++;
|
||||
break;
|
||||
case 'tag':
|
||||
$(listId).append(getTagBlock(i.item, entities));
|
||||
$(listId).append(getTagBlock(i.item, i.entities, i.hidden));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const hidden = (characters.length + groups.length) - displayCount;
|
||||
if (hidden > 0) {
|
||||
$(listId).append(getHiddenBlock(hidden));
|
||||
}
|
||||
|
||||
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||
},
|
||||
afterSizeSelectorChange: function (e) {
|
||||
@ -1314,15 +1324,7 @@ async function printCharacters(fullRefresh = false) {
|
||||
favsToHotswap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether a user is currently in a bogus folder.
|
||||
* @returns {boolean} If currently viewing a folder
|
||||
*/
|
||||
function isBogusFolderOpen() {
|
||||
return !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus;
|
||||
}
|
||||
|
||||
export function getEntitiesList({ doFilter } = {}) {
|
||||
export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
||||
function characterToEntity(character, id) {
|
||||
return { item: character, id, type: 'character' };
|
||||
}
|
||||
@ -1332,36 +1334,53 @@ export function getEntitiesList({ doFilter } = {}) {
|
||||
}
|
||||
|
||||
function tagToEntity(tag) {
|
||||
return { item: structuredClone(tag), id: tag.id, type: 'tag' };
|
||||
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
|
||||
}
|
||||
|
||||
let entities = [
|
||||
...characters.map((item, index) => characterToEntity(item, index)),
|
||||
...groups.map(item => groupToEntity(item)),
|
||||
...(power_user.bogus_folders ? tags.map(item => tagToEntity(item)) : []),
|
||||
...(power_user.bogus_folders ? tags.filter(isBogusFolder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []),
|
||||
];
|
||||
|
||||
// We need to do multiple filter runs in a specific order, otherwise different settings might override each other
|
||||
// and screw up tags and search filter, sub lists or similar.
|
||||
// The specific filters are written inside the "filterByTagState" method and its different parameters.
|
||||
// Generally what we do is the following:
|
||||
// 1. First swipe over the list to remove the most obvious things
|
||||
// 2. Build sub entity lists for all folders, filtering them similarly to the second swipe
|
||||
// 3. We do the last run, where global filters are applied, and the search filters last
|
||||
|
||||
// First run filters, that will hide what should never be displayed
|
||||
if (doFilter) {
|
||||
entities = filterByTagState(entities);
|
||||
}
|
||||
|
||||
// Run over all entities between first and second filter to save some states
|
||||
for (const entity of entities) {
|
||||
// For folders, we remember the sub entities so they can be displayed later, even if they might be filtered
|
||||
// Those sub entities should be filtered and have the search filters applied too
|
||||
if (entity.type === 'tag') {
|
||||
let subEntities = filterByTagState(entities, { subForEntity: entity, filterHidden: false });
|
||||
const subCount = subEntities.length;
|
||||
subEntities = filterByTagState(entities, { subForEntity: entity });
|
||||
if (doFilter) {
|
||||
subEntities = entitiesFilter.applyFilters(subEntities);
|
||||
}
|
||||
entity.entities = subEntities;
|
||||
entity.hidden = subCount - subEntities.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Second run filters, hiding whatever should be filtered later
|
||||
if (doFilter) {
|
||||
entities = filterByTagState(entities, { globalDisplayFilters: true });
|
||||
entities = entitiesFilter.applyFilters(entities);
|
||||
}
|
||||
|
||||
if (isBogusFolderOpen()) {
|
||||
// Get tags of entities within the bogus folder
|
||||
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
|
||||
entities = entities.filter(x => x.type !== 'tag');
|
||||
const otherTags = tags.filter(x => !filterData.selected.includes(x.id));
|
||||
const bogusTags = [];
|
||||
for (const entity of entities) {
|
||||
for (const tag of otherTags) {
|
||||
if (!bogusTags.includes(tag) && entitiesFilter.isElementTagged(entity, tag.id)) {
|
||||
bogusTags.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
entities.push(...bogusTags.map(item => tagToEntity(item)));
|
||||
if (doSort) {
|
||||
sortEntitiesList(entities);
|
||||
}
|
||||
|
||||
sortEntitiesList(entities);
|
||||
return entities;
|
||||
}
|
||||
|
||||
@ -5261,6 +5280,51 @@ function getThumbnailUrl(type, file) {
|
||||
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
|
||||
}
|
||||
|
||||
function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) {
|
||||
if (empty) {
|
||||
block.empty();
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
const id = entity.id;
|
||||
|
||||
// Populate the template
|
||||
const avatarTemplate = $(`#${templateId} .avatar`).clone();
|
||||
|
||||
let this_avatar = default_avatar;
|
||||
if (entity.item.avatar !== undefined && entity.item.avatar != 'none') {
|
||||
this_avatar = getThumbnailUrl('avatar', entity.item.avatar);
|
||||
}
|
||||
|
||||
avatarTemplate.attr('data-type', entity.type);
|
||||
avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` });
|
||||
avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name);
|
||||
avatarTemplate.attr('title', `[Character] ${entity.item.name}`);
|
||||
if (highlightFavs) {
|
||||
avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true');
|
||||
avatarTemplate.find('.ch_fav').val(entity.item.fav);
|
||||
}
|
||||
|
||||
// If this is a group, we need to hack slightly. We still want to keep most of the css classes and layout, but use a group avatar instead.
|
||||
if (entity.type === 'group') {
|
||||
const grpTemplate = getGroupAvatar(entity.item);
|
||||
|
||||
avatarTemplate.addClass(grpTemplate.attr('class'));
|
||||
avatarTemplate.empty();
|
||||
avatarTemplate.append(grpTemplate.children());
|
||||
avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
|
||||
}
|
||||
|
||||
if (selectable) {
|
||||
avatarTemplate.addClass('selectable');
|
||||
avatarTemplate.toggleClass('character_select', entity.type === 'character');
|
||||
avatarTemplate.toggleClass('group_select', entity.type === 'group');
|
||||
}
|
||||
|
||||
block.append(avatarTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
async function getChat() {
|
||||
//console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
|
||||
try {
|
||||
@ -5287,9 +5351,12 @@ async function getChat() {
|
||||
await getChatResult();
|
||||
eventSource.emit('chatLoaded', { detail: { id: this_chid, character: characters[this_chid] } });
|
||||
|
||||
// Focus on the textarea if not already focused on a visible text input
|
||||
setTimeout(function () {
|
||||
$('#send_textarea').click();
|
||||
$('#send_textarea').focus();
|
||||
if ($(document.activeElement).is('input:visible, textarea:visible')) {
|
||||
return;
|
||||
}
|
||||
$('#send_textarea').trigger('click').trigger('focus');
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
await getChatResult();
|
||||
@ -8271,25 +8338,8 @@ jQuery(async function () {
|
||||
|
||||
$(document).on('click', '.bogus_folder_select', function () {
|
||||
const tagId = $(this).attr('tagid');
|
||||
console.log('Bogus folder clicked', tagId);
|
||||
|
||||
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
|
||||
|
||||
if (!Array.isArray(filterData.selected)) {
|
||||
filterData.selected = [];
|
||||
filterData.excluded = [];
|
||||
filterData.bogus = false;
|
||||
}
|
||||
|
||||
if (tagId === 'back') {
|
||||
filterData.selected.pop();
|
||||
filterData.bogus = filterData.selected.length > 0;
|
||||
} else {
|
||||
filterData.selected.push(tagId);
|
||||
filterData.bogus = true;
|
||||
}
|
||||
|
||||
entitiesFilter.setFilterData(FILTER_TYPES.TAG, filterData);
|
||||
console.debug('Bogus folder clicked', tagId);
|
||||
chooseBogusFolder($(this), tagId);
|
||||
});
|
||||
|
||||
$(document).on('input', '.edit_textarea', function () {
|
||||
|
@ -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] = [];
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
setActiveGroup,
|
||||
setActiveCharacter,
|
||||
getEntitiesList,
|
||||
getThumbnailUrl,
|
||||
buildAvatarList,
|
||||
selectCharacterById,
|
||||
eventSource,
|
||||
menu_type,
|
||||
@ -26,7 +26,8 @@ import {
|
||||
} from './power-user.js';
|
||||
|
||||
import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
|
||||
import { selected_group, is_group_generating, getGroupAvatar, groups, openGroupById } from './group-chats.js';
|
||||
import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
|
||||
import { getTagKeyForEntity } from './tags.js';
|
||||
import {
|
||||
SECRET_KEYS,
|
||||
secret_state,
|
||||
@ -247,13 +248,14 @@ export function RA_CountCharTokens() {
|
||||
async function RA_autoloadchat() {
|
||||
if (document.querySelector('#rm_print_characters_block .character_select') !== null) {
|
||||
// active character is the name, we should look it up in the character list and get the id
|
||||
let active_character_id = Object.keys(characters).find(key => characters[key].avatar === active_character);
|
||||
|
||||
if (active_character_id !== null) {
|
||||
await selectCharacterById(String(active_character_id));
|
||||
if (active_character !== null && active_character !== undefined) {
|
||||
const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
|
||||
if (active_character_id !== null) {
|
||||
await selectCharacterById(String(active_character_id));
|
||||
}
|
||||
}
|
||||
|
||||
if (active_group != null) {
|
||||
if (active_group !== null && active_group !== undefined) {
|
||||
await openGroupById(String(active_group));
|
||||
}
|
||||
|
||||
@ -264,84 +266,16 @@ async function RA_autoloadchat() {
|
||||
export async function favsToHotswap() {
|
||||
const entities = getEntitiesList({ doFilter: false });
|
||||
const container = $('#right-nav-panel .hotswap');
|
||||
const template = $('#hotswap_template .hotswapAvatar');
|
||||
const DEFAULT_COUNT = 6;
|
||||
const WIDTH_PER_ITEM = 60; // 50px + 5px gap + 5px padding
|
||||
const containerWidth = container.outerWidth();
|
||||
const maxCount = containerWidth > 0 ? Math.floor(containerWidth / WIDTH_PER_ITEM) : DEFAULT_COUNT;
|
||||
let count = 0;
|
||||
|
||||
const promises = [];
|
||||
const newContainer = container.clone();
|
||||
newContainer.empty();
|
||||
const favs = entities.filter(x => x.item.fav || x.item.fav == 'true');
|
||||
|
||||
for (const entity of entities) {
|
||||
if (count >= maxCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
const isFavorite = entity.item.fav || entity.item.fav == 'true';
|
||||
|
||||
if (!isFavorite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isCharacter = entity.type === 'character';
|
||||
const isGroup = entity.type === 'group';
|
||||
|
||||
const grid = isGroup ? entity.id : '';
|
||||
const chid = isCharacter ? entity.id : '';
|
||||
|
||||
let slot = template.clone();
|
||||
slot.toggleClass('character_select', isCharacter);
|
||||
slot.toggleClass('group_select', isGroup);
|
||||
slot.attr('grid', isGroup ? grid : '');
|
||||
slot.attr('chid', isCharacter ? chid : '');
|
||||
slot.data('id', isGroup ? grid : chid);
|
||||
|
||||
if (isGroup) {
|
||||
const group = groups.find(x => x.id === grid);
|
||||
const avatar = getGroupAvatar(group);
|
||||
$(slot).find('img').replaceWith(avatar);
|
||||
$(slot).attr('title', group.name);
|
||||
}
|
||||
|
||||
if (isCharacter) {
|
||||
const imgLoadPromise = new Promise((resolve) => {
|
||||
const avatarUrl = getThumbnailUrl('avatar', entity.item.avatar);
|
||||
$(slot).find('img').attr('src', avatarUrl).on('load', resolve);
|
||||
$(slot).attr('title', entity.item.avatar);
|
||||
});
|
||||
|
||||
// if the image doesn't load in 500ms, resolve the promise anyway
|
||||
promises.push(Promise.race([imgLoadPromise, delay(500)]));
|
||||
}
|
||||
|
||||
$(slot).css('cursor', 'pointer');
|
||||
newContainer.append(slot);
|
||||
count++;
|
||||
}
|
||||
|
||||
// don't fill leftover spaces with avatar placeholders
|
||||
// just evenly space the selected avatars instead
|
||||
/*
|
||||
if (count < maxCount) { //if any space is left over
|
||||
let leftOverSlots = maxCount - count;
|
||||
for (let i = 1; i <= leftOverSlots; i++) {
|
||||
newContainer.append(template.clone());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
//helpful instruction message if no characters are favorited
|
||||
if (count === 0) {
|
||||
container.html('<small><span data-i18n="Favorite characters to add them to HotSwaps"><i class="fa-solid fa-star"></i> Favorite characters to add them to HotSwaps</span></small>');
|
||||
}
|
||||
//otherwise replace with fav'd characters
|
||||
if (count > 0) {
|
||||
container.replaceWith(newContainer);
|
||||
if (favs.length == 0) {
|
||||
container.html('<small><span><i class="fa-solid fa-star"></i> <span data-i18n="Favorite characters to add them to HotSwaps">Favorite characters to add them to HotSwaps</span></span></small>');
|
||||
return;
|
||||
}
|
||||
|
||||
buildAvatarList(container, favs, { selectable: true, highlightFavs: false });
|
||||
}
|
||||
|
||||
//changes input bar and send button display depending on connection status
|
||||
@ -873,14 +807,14 @@ export function initRossMods() {
|
||||
|
||||
// when a char is selected from the list, save their name as the auto-load character for next page load
|
||||
$(document).on('click', '.character_select', function () {
|
||||
const characterId = $(this).find('.avatar').attr('title') || $(this).attr('title');
|
||||
const characterId = $(this).attr('chid') || $(this).data('id');
|
||||
setActiveCharacter(characterId);
|
||||
setActiveGroup(null);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('click', '.group_select', function () {
|
||||
const groupId = $(this).data('id') || $(this).attr('grid');
|
||||
const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id');
|
||||
setActiveCharacter(null);
|
||||
setActiveGroup(groupId);
|
||||
saveSettingsDebounced();
|
||||
|
@ -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.<string, Object>}
|
||||
*/
|
||||
export const FILTER_STATES = {
|
||||
SELECTED: { key: 'SELECTED', class: 'selected' },
|
||||
EXCLUDED: { key: 'EXCLUDED', class: 'excluded' },
|
||||
UNDEFINED: { key: 'UNDEFINED', class: 'undefined' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Robust check if one state equals the other. It does not care whether it's the state key or the state value object.
|
||||
* @param {Object} a First state
|
||||
* @param {Object} b Second state
|
||||
*/
|
||||
export function isFilterState(a, b) {
|
||||
const states = Object.keys(FILTER_STATES);
|
||||
|
||||
const aKey = states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a);
|
||||
const bKey = states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b);
|
||||
|
||||
return aKey === bKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for filtering data.
|
||||
* @example
|
||||
@ -36,8 +61,9 @@ export class FilterHelper {
|
||||
*/
|
||||
filterFunctions = {
|
||||
[FILTER_TYPES.SEARCH]: this.searchFilter.bind(this),
|
||||
[FILTER_TYPES.GROUP]: this.groupFilter.bind(this),
|
||||
[FILTER_TYPES.FAV]: this.favFilter.bind(this),
|
||||
[FILTER_TYPES.GROUP]: this.groupFilter.bind(this),
|
||||
[FILTER_TYPES.FOLDER]: this.folderFilter.bind(this),
|
||||
[FILTER_TYPES.TAG]: this.tagFilter.bind(this),
|
||||
[FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.bind(this),
|
||||
[FILTER_TYPES.PERSONA_SEARCH]: this.personaSearchFilter.bind(this),
|
||||
@ -49,8 +75,9 @@ export class FilterHelper {
|
||||
*/
|
||||
filterData = {
|
||||
[FILTER_TYPES.SEARCH]: '',
|
||||
[FILTER_TYPES.GROUP]: false,
|
||||
[FILTER_TYPES.FAV]: false,
|
||||
[FILTER_TYPES.GROUP]: false,
|
||||
[FILTER_TYPES.FOLDER]: false,
|
||||
[FILTER_TYPES.TAG]: { excluded: [], selected: [] },
|
||||
[FILTER_TYPES.WORLD_INFO_SEARCH]: '',
|
||||
[FILTER_TYPES.PERSONA_SEARCH]: '',
|
||||
@ -116,6 +143,7 @@ export class FilterHelper {
|
||||
}
|
||||
|
||||
const getIsTagged = (entity) => {
|
||||
const isTag = entity.type === 'tag';
|
||||
const tagFlags = selected.map(tagId => this.isElementTagged(entity, tagId));
|
||||
const trueFlags = tagFlags.filter(x => x);
|
||||
const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0;
|
||||
@ -123,7 +151,9 @@ export class FilterHelper {
|
||||
const excludedTagFlags = excluded.map(tagId => this.isElementTagged(entity, tagId));
|
||||
const isExcluded = excludedTagFlags.includes(true);
|
||||
|
||||
if (isExcluded) {
|
||||
if (isTag) {
|
||||
return true;
|
||||
} else if (isExcluded) {
|
||||
return false;
|
||||
} else if (selected.length > 0 && !isTagged) {
|
||||
return false;
|
||||
@ -141,11 +171,10 @@ export class FilterHelper {
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
favFilter(data) {
|
||||
if (!this.filterData[FILTER_TYPES.FAV]) {
|
||||
return data;
|
||||
}
|
||||
const state = this.filterData[FILTER_TYPES.FAV];
|
||||
const isFav = entity => entity.item.fav || entity.item.fav == 'true';
|
||||
|
||||
return data.filter(entity => entity.item.fav || entity.item.fav == 'true');
|
||||
return this.filterDataByState(data, state, isFav, { includeFolders: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,11 +183,35 @@ export class FilterHelper {
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
groupFilter(data) {
|
||||
if (!this.filterData[FILTER_TYPES.GROUP]) {
|
||||
return data;
|
||||
const state = this.filterData[FILTER_TYPES.GROUP];
|
||||
const isGroup = entity => entity.type === 'group';
|
||||
|
||||
return this.filterDataByState(data, state, isGroup, { includeFolders: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a "folder" filter to the data.
|
||||
* @param {any[]} data The data to filter.
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
folderFilter(data) {
|
||||
const state = this.filterData[FILTER_TYPES.FOLDER];
|
||||
// Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
|
||||
// while a negative state should then filter out all folders.
|
||||
const isFolder = entity => isFilterState(state, FILTER_STATES.SELECTED) ? true : entity.type === 'tag';
|
||||
|
||||
return this.filterDataByState(data, state, isFolder);
|
||||
}
|
||||
|
||||
filterDataByState(data, state, filterFunc, { includeFolders } = {}) {
|
||||
if (isFilterState(state, FILTER_STATES.SELECTED)) {
|
||||
return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag'));
|
||||
}
|
||||
if (isFilterState(state, FILTER_STATES.EXCLUDED)) {
|
||||
return data.filter(entity => !filterFunc(entity) || (includeFolders && entity.type == 'tag'));
|
||||
}
|
||||
|
||||
return data.filter(entity => entity.type === 'group');
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,7 +69,7 @@ import {
|
||||
loadItemizedPrompts,
|
||||
animation_duration,
|
||||
} from '../script.js';
|
||||
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
|
||||
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
|
||||
export {
|
||||
@ -530,18 +530,33 @@ async function getGroups() {
|
||||
}
|
||||
|
||||
export function getGroupBlock(group) {
|
||||
let count = 0;
|
||||
let namesList = [];
|
||||
|
||||
// Build inline name list
|
||||
if (Array.isArray(group.members) && group.members.length) {
|
||||
for (const member of group.members) {
|
||||
const character = characters.find(x => x.avatar === member || x.name === member);
|
||||
if (character) {
|
||||
namesList.push(character.name);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const template = $('#group_list_template .group_select').clone();
|
||||
template.data('id', group.id);
|
||||
template.attr('grid', group.id);
|
||||
template.find('.ch_name').text(group.name);
|
||||
template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`);
|
||||
template.find('.group_fav_icon').css('display', 'none');
|
||||
template.addClass(group.fav ? 'is_fav' : '');
|
||||
template.find('.ch_fav').val(group.fav);
|
||||
template.find('.group_select_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
|
||||
template.find('.group_select_block_list').text(namesList.join(', '));
|
||||
|
||||
// Display inline tags
|
||||
const tags = getTagsList(group.id);
|
||||
const tagsElement = template.find('.tags');
|
||||
tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
|
||||
printTagList(tagsElement, { forEntityOrKey: group.id });
|
||||
|
||||
const avatar = getGroupAvatar(group);
|
||||
if (avatar) {
|
||||
@ -559,6 +574,8 @@ function updateGroupAvatar(group) {
|
||||
$(this).find('.avatar').replaceWith(getGroupAvatar(group));
|
||||
}
|
||||
});
|
||||
|
||||
favsToHotswap();
|
||||
}
|
||||
|
||||
// check if isDataURLor if it's a valid local file url
|
||||
@ -576,7 +593,7 @@ function getGroupAvatar(group) {
|
||||
}
|
||||
// if isDataURL or if it's a valid local file url
|
||||
if (isValidImageUrl(group.avatar_url)) {
|
||||
return $(`<div class="avatar"><img src="${group.avatar_url}"></div>`);
|
||||
return $(`<div class="avatar" title="[Group] ${group.name}"><img src="${group.avatar_url}"></div>`);
|
||||
}
|
||||
|
||||
const memberAvatars = [];
|
||||
@ -602,6 +619,7 @@ function getGroupAvatar(group) {
|
||||
groupAvatar.find(`.img_${i + 1}`).attr('src', memberAvatars[i]);
|
||||
}
|
||||
|
||||
groupAvatar.attr('title', `[Group] ${group.name}`);
|
||||
return groupAvatar;
|
||||
}
|
||||
|
||||
@ -613,6 +631,7 @@ function getGroupAvatar(group) {
|
||||
// default avatar
|
||||
const groupAvatar = $('#group_avatars_template .collage_1').clone();
|
||||
groupAvatar.find('.img_1').attr('src', group.avatar_url || system_avatar);
|
||||
groupAvatar.attr('title', `[Group] ${group.name}`);
|
||||
return groupAvatar;
|
||||
}
|
||||
|
||||
@ -1176,9 +1195,8 @@ function getGroupCharacterBlock(character) {
|
||||
template.toggleClass('disabled', isGroupMemberDisabled(character.avatar));
|
||||
|
||||
// Display inline tags
|
||||
const tags = getTagsList(character.avatar);
|
||||
const tagsElement = template.find('.tags');
|
||||
tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
|
||||
printTagList(tagsElement, { forEntityOrKey: characters.indexOf(character) });
|
||||
|
||||
if (!openGroupId) {
|
||||
template.find('[data-action="speak"]').hide();
|
||||
@ -1254,6 +1272,9 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
selectRightMenuWithAnimation('rm_group_chats_block');
|
||||
}
|
||||
|
||||
// render tags
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
|
||||
|
||||
// render characters list
|
||||
printGroupCandidates();
|
||||
printGroupMembers();
|
||||
@ -1751,7 +1772,7 @@ function doCurMemberListPopout() {
|
||||
|
||||
jQuery(() => {
|
||||
$(document).on('click', '.group_select', function () {
|
||||
const groupId = $(this).data('id');
|
||||
const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id');
|
||||
openGroupById(groupId);
|
||||
});
|
||||
$('#rm_group_filter').on('input', filterGroupMembers);
|
||||
|
@ -235,6 +235,7 @@ let power_user = {
|
||||
encode_tags: false,
|
||||
servers: [],
|
||||
bogus_folders: false,
|
||||
show_tag_filters: false,
|
||||
aux_field: 'character_version',
|
||||
restore_user_input: true,
|
||||
reduced_motion: false,
|
||||
|
@ -7,23 +7,35 @@ import {
|
||||
getCharacters,
|
||||
entitiesFilter,
|
||||
printCharacters,
|
||||
buildAvatarList,
|
||||
} from '../script.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4 } from './utils.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
|
||||
export {
|
||||
TAG_FOLDER_TYPES,
|
||||
TAG_FOLDER_DEFAULT_TYPE,
|
||||
tags,
|
||||
tag_map,
|
||||
filterByTagState,
|
||||
isBogusFolder,
|
||||
isBogusFolderOpen,
|
||||
chooseBogusFolder,
|
||||
getTagBlock,
|
||||
loadTagsSettings,
|
||||
printTagFilters,
|
||||
getTagsList,
|
||||
printTagList,
|
||||
appendTagToList,
|
||||
createTagMapFromList,
|
||||
renameTagKey,
|
||||
importTags,
|
||||
sortTags,
|
||||
compareTagsForSort,
|
||||
};
|
||||
|
||||
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
|
||||
@ -33,16 +45,24 @@ function getFilterHelper(listSelector) {
|
||||
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
|
||||
}
|
||||
|
||||
const redrawCharsAndFiltersDebounced = debounce(() => {
|
||||
printCharacters(false);
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
}, 100);
|
||||
|
||||
export const tag_filter_types = {
|
||||
character: 0,
|
||||
group_member: 1,
|
||||
};
|
||||
|
||||
const ACTIONABLE_TAGS = {
|
||||
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
|
||||
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
|
||||
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
|
||||
FOLDER: { id: 4, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
|
||||
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
|
||||
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
|
||||
UNFILTER: { id: 5, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' },
|
||||
};
|
||||
|
||||
const InListActionable = {
|
||||
@ -57,19 +77,170 @@ const DEFAULT_TAGS = [
|
||||
{ id: uuidv4(), name: 'AliChat', create_date: Date.now() },
|
||||
];
|
||||
|
||||
const TAG_FOLDER_TYPES = {
|
||||
OPEN: { icon: '✔', class: 'folder_open', fa_icon: 'fa-folder-open', tooltip: 'Open Folder (Show all characters even if not selected)', color: 'green', size: '1' },
|
||||
CLOSED: { icon: '👁', class: 'folder_closed', fa_icon: 'fa-eye-slash', tooltip: 'Closed Folder (Hide all characters unless selected)', color: 'lightgoldenrodyellow', size: '0.7' },
|
||||
NONE: { icon: '✕', class: 'no_folder', tooltip: 'No Folder', color: 'red', size: '1' },
|
||||
};
|
||||
const TAG_FOLDER_DEFAULT_TYPE = 'NONE';
|
||||
|
||||
|
||||
let tags = [];
|
||||
let tag_map = {};
|
||||
|
||||
/**
|
||||
* Applies the basic filter for the current state of the tags and their selection on an entity list.
|
||||
* @param {Array<Object>} 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.
|
||||
* @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, filterHidden = true } = {}) {
|
||||
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 (filterHidden && 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
|
||||
const alwaysFolder = isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED);
|
||||
if (entity.type === 'tag') {
|
||||
return alwaysFolder || entity.entities.length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (subForEntity !== undefined && subForEntity.type === 'tag') {
|
||||
entities = filterTagSubEntities(subForEntity.item, entities, { filterHidden : filterHidden });
|
||||
}
|
||||
|
||||
return 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);
|
||||
|
||||
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 (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;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
||||
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_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);
|
||||
|
||||
// Fill inline character images
|
||||
buildAvatarList(template.find('.bogus_folder_avatars_block'), entities);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,11 +248,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) {
|
||||
@ -111,7 +288,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() {
|
||||
@ -138,12 +315,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;
|
||||
@ -159,7 +361,7 @@ function addTagToMap(tagId, characterId = null) {
|
||||
}
|
||||
|
||||
function removeTagFromMap(tagId, characterId = null) {
|
||||
const key = getTagKey() ?? getTagKeyForCharacter(characterId);
|
||||
const key = getTagKey() ?? getTagKeyForEntity(characterId);
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
@ -200,10 +402,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;
|
||||
@ -215,6 +413,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);
|
||||
|
||||
@ -245,7 +448,9 @@ async function importTags(imported_char) {
|
||||
} else {
|
||||
selected_tags = await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p><p>The following ${newTags.length} new tags will be imported.</p>`, '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 +481,8 @@ function createNewTag(tagName) {
|
||||
const tag = {
|
||||
id: uuidv4(),
|
||||
name: tagName,
|
||||
folder_type: TAG_FOLDER_DEFAULT_TYPE,
|
||||
sort_order: tags.length,
|
||||
color: '',
|
||||
color2: '',
|
||||
create_date: Date.now(),
|
||||
@ -284,18 +491,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<HTMLElement>} element - The container element where the tags are to be printed.
|
||||
* @param {object} [options] - Optional parameters for printing the tag list.
|
||||
* @param {Array<object>} [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<HTMLElement>} 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);
|
||||
@ -317,7 +569,7 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
|
||||
}
|
||||
|
||||
if (tag.excluded && isGeneralList) {
|
||||
$(tagElement).addClass('excluded');
|
||||
toggleTagThreeState(tagElement, { stateOverride: FILTER_STATES.EXCLUDED });
|
||||
}
|
||||
|
||||
if (selectable) {
|
||||
@ -337,32 +589,67 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
|
||||
}
|
||||
|
||||
function onTagFilterClick(listElement) {
|
||||
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');
|
||||
}
|
||||
const tagId = $(this).attr('id');
|
||||
const existingTag = tags.find((tag) => tag.id === tagId);
|
||||
|
||||
let state = toggleTagThreeState($(this));
|
||||
|
||||
// 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;
|
||||
if (existingTag) {
|
||||
existingTag.excluded = isFilterState(state, FILTER_STATES.EXCLUDED);
|
||||
|
||||
saveSettingsDebounced();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
// Update bogus folder if applicable
|
||||
if (isBogusFolder(existingTag)) {
|
||||
// Update bogus drilldown
|
||||
if ($(this).hasClass('selected')) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
runTagFilters(listElement);
|
||||
updateTagFilterIndicator();
|
||||
}
|
||||
|
||||
function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) {
|
||||
const states = Object.keys(FILTER_STATES);
|
||||
|
||||
const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride);
|
||||
|
||||
const currentStateIndex = states.indexOf(element.attr('data-toggle-state')) ?? states.length - 1;
|
||||
const targetStateIndex = overrideKey !== undefined ? states.indexOf(overrideKey) : (currentStateIndex + 1) % states.length;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -373,32 +660,44 @@ 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((a, b) => a.name.localeCompare(b.name));
|
||||
.sort(compareTagsForSort);
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } });
|
||||
|
||||
for (const tag of Object.values(ACTIONABLE_TAGS)) {
|
||||
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true });
|
||||
runTagFilters(FILTER_SELECTOR);
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
$(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 });
|
||||
}
|
||||
for (const tag of tagsToDisplay) {
|
||||
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: true, isGeneralList: true });
|
||||
if (tag.excluded) {
|
||||
runTagFilters(FILTER_SELECTOR);
|
||||
}
|
||||
if (power_user.show_tag_filters) {
|
||||
$('.rm_tag_controls .showTagList').addClass('selected');
|
||||
$('.rm_tag_controls').find('.tag:not(.actionable)').show();
|
||||
}
|
||||
|
||||
for (const tagId of selectedTagIds) {
|
||||
$(`${FILTER_SELECTOR} .tag[id="${tagId}"]`).trigger('click');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,6 +706,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($(this), 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;
|
||||
@ -426,13 +732,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());
|
||||
}
|
||||
|
||||
@ -441,40 +750,23 @@ 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) {
|
||||
$(inputSelector)
|
||||
// @ts-ignore
|
||||
.autocomplete({
|
||||
source: (i, o) => findTag(i, o, listSelector),
|
||||
select: (e, u) => selectTag(e, u, listSelector),
|
||||
@ -509,19 +801,86 @@ function onViewTagsListClick() {
|
||||
</div>
|
||||
<div class="justifyLeft m-b-1">
|
||||
<small>
|
||||
Drag the handle to reorder.<br>
|
||||
${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder.<br>' : '')}
|
||||
Click on the tag name to edit it.<br>
|
||||
Click on color box to assign new color.
|
||||
</small>
|
||||
</div>`);
|
||||
|
||||
const sortedTags = tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()));
|
||||
const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
|
||||
list.append(tagContainer);
|
||||
|
||||
const sortedTags = sortTags(tags);
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
appendViewTagToList(list, tag, everything);
|
||||
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. Do it debounced so it doesn't block and you can drag multiple tags.
|
||||
redrawCharsAndFiltersDebounced();
|
||||
};
|
||||
|
||||
// @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 +979,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 +995,14 @@ 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('.tagColorPickerHolder').html(
|
||||
`<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`,
|
||||
);
|
||||
@ -646,6 +1010,7 @@ function appendViewTagToList(list, tag, everything) {
|
||||
`<toolcool-color-picker id="${colorPicker2Id}" color="${tag.color2}" class="tag-color2"></toolcool-color-picker>`,
|
||||
);
|
||||
|
||||
template.find('.tag_as_folder').attr('id', tagAsFolderId);
|
||||
template.find('.tag-color').attr('id', colorPickerId);
|
||||
template.find('.tag-color2').attr('id', colorPicker2Id);
|
||||
|
||||
@ -663,10 +1028,49 @@ function appendViewTagToList(list, tag, everything) {
|
||||
});
|
||||
}, 100);
|
||||
|
||||
updateDrawTagFolder(template, tag);
|
||||
|
||||
// @ts-ignore
|
||||
$(colorPickerId).color = tag.color;
|
||||
// @ts-ignore
|
||||
$(colorPicker2Id).color = tag.color2;
|
||||
}
|
||||
|
||||
function onTagAsFolderClick() {
|
||||
const element = $(this).closest('.tag_view_item');
|
||||
const id = element.attr('id');
|
||||
const tag = tags.find(x => x.id === id);
|
||||
|
||||
// Cycle through folder types
|
||||
const types = Object.keys(TAG_FOLDER_TYPES);
|
||||
const 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);
|
||||
saveSettingsDebounced();
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
@ -720,10 +1124,31 @@ function onTagColorize2(evt) {
|
||||
}
|
||||
|
||||
function onTagListHintClick() {
|
||||
console.log($(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.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(() => {
|
||||
@ -738,8 +1163,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);
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
@ -2055,7 +2055,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];
|
||||
|
186
public/style.css
186
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-small-factor: 0.6;
|
||||
}
|
||||
|
||||
* {
|
||||
@ -854,8 +860,8 @@ body.reduced-motion #bg_custom {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@ -865,54 +871,42 @@ body.reduced-motion #bg_custom {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
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 {
|
||||
padding-bottom: 50px;
|
||||
padding-bottom: var(--avatar-base-height);
|
||||
}
|
||||
|
||||
.mes .avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#HotSwapWrapper .hotswap {
|
||||
.hotswap {
|
||||
margin: 5px;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.hotswapAvatar,
|
||||
.hotswapAvatar .avatar {
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
border-style: none;
|
||||
#HotSwapWrapper {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.hotswapAvatar {
|
||||
opacity: 0.5;
|
||||
transition: 250ms;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
order: 100;
|
||||
.avatar.selectable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.hotswapAvatar:hover {
|
||||
.avatar.selectable:hover {
|
||||
opacity: 1;
|
||||
background-color: transparent !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hotswapAvatar .avatar_collage,
|
||||
.hotswapAvatar.group_select {
|
||||
border-radius: 50% !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-width: 50px !important;
|
||||
}
|
||||
|
||||
.hotswapAvatar.group_select .avatar.avatar_collage img {
|
||||
.avatar.avatar_collage img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
@ -920,32 +914,29 @@ body.reduced-motion #bg_custom {
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
.hotswapAvatar .avatar {
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
border-radius: 50% !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);
|
||||
}
|
||||
|
||||
.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);
|
||||
@ -955,6 +946,30 @@ body.reduced-motion #bg_custom {
|
||||
outline-color: var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.avatars_inline .avatar:last-of-type {
|
||||
margin-right: calc(var(--avatar-base-border-radius));
|
||||
}
|
||||
|
||||
.mes_block {
|
||||
padding-top: 0;
|
||||
padding-left: 10px;
|
||||
@ -1289,20 +1304,32 @@ 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;
|
||||
}
|
||||
|
||||
.hidden_block {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#rm_print_characters_block .hidden_block p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#rm_print_characters_block {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
@ -1587,6 +1614,12 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.character_select.inline_avatar,
|
||||
.missing-avatar.inline_avatar {
|
||||
padding: unset;
|
||||
border-radius: var(--avatar-base-border-radius-round);
|
||||
}
|
||||
|
||||
/*applies to char list and mes_text char display name*/
|
||||
|
||||
.ch_name {
|
||||
@ -1598,6 +1631,19 @@ 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);
|
||||
}
|
||||
|
||||
.character_name_block .ch_add_placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ch_avatar_url {
|
||||
@ -1623,15 +1669,29 @@ 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 {
|
||||
@ -1802,7 +1862,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);
|
||||
}
|
||||
|
||||
@ -1866,8 +1927,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 {
|
||||
@ -2321,18 +2382,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 {
|
||||
@ -2989,8 +3050,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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user