Add pagination for characters list view

This commit is contained in:
Cohee
2023-08-18 23:13:15 +03:00
parent 303f961ee2
commit 602c5cd791
9 changed files with 1610 additions and 347 deletions

View File

@@ -13,11 +13,13 @@ import {
menu_type,
max_context,
saveSettingsDebounced,
eventSource,
active_group,
active_character,
setActiveGroup,
setActiveCharacter,
getEntitiesList,
getThumbnailUrl,
selectCharacterById,
} from "../script.js";
import {
@@ -348,12 +350,13 @@ async function RA_autoloadchat() {
// 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);
var charToAutoLoad = document.getElementById('CharID' + active_character_id);
let groupToAutoLoad = document.querySelector(`.group_select[grid="${active_group}"]`);
if (charToAutoLoad != null) {
$(charToAutoLoad).click();
if (active_character_id !== null) {
selectCharacterById(String(active_character_id));
}
else if (groupToAutoLoad != null) {
let groupToAutoLoad = document.querySelector(`.group_select[grid="${active_group}"]`);
if (groupToAutoLoad != null) {
$(groupToAutoLoad).click();
}
@@ -362,53 +365,60 @@ async function RA_autoloadchat() {
}
export async function favsToHotswap() {
const selector = ['#rm_print_characters_block .character_select', '#rm_print_characters_block .group_select'].join(',');
const entities = getEntitiesList({ doFilter: false });
const container = $('#right-nav-panel .hotswap');
const template = $('#hotswap_template .hotswapAvatar');
container.empty();
const maxCount = 6;
let count = 0;
$(selector).sort(sortByCssOrder).each(function () {
if ($(this).hasClass('is_fav') && count < maxCount) {
const isCharacter = $(this).hasClass('character_select');
const isGroup = $(this).hasClass('group_select');
const grid = Number($(this).attr('grid'));
const chid = Number($(this).attr('chid'));
let thisHotSwapSlot = template.clone();
thisHotSwapSlot.toggleClass('character_select', isCharacter);
thisHotSwapSlot.toggleClass('group_select', isGroup);
thisHotSwapSlot.attr('grid', isGroup ? grid : '');
thisHotSwapSlot.attr('chid', isCharacter ? chid : '');
thisHotSwapSlot.data('id', isGroup ? grid : chid);
thisHotSwapSlot.attr('title', '');
if (isGroup) {
const group = groups.find(x => x.id === grid);
const avatar = getGroupAvatar(group);
$(thisHotSwapSlot).find('img').replaceWith(avatar);
}
if (isCharacter) {
const avatarUrl = $(this).find('img').attr('src');
$(thisHotSwapSlot).find('img').attr('src', avatarUrl);
}
$(thisHotSwapSlot).css('cursor', 'pointer');
container.append(thisHotSwapSlot);
count++;
for (const entity of entities) {
if (count >= maxCount) {
break;
}
});
//console.log('about to check for leftover selectors...')
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);
slot.attr('title', '');
if (isGroup) {
const group = groups.find(x => x.id === grid);
const avatar = getGroupAvatar(group);
$(slot).find('img').replaceWith(avatar);
}
if (isCharacter) {
const avatarUrl = getThumbnailUrl('avatar', entity.item.avatar);
$(slot).find('img').attr('src', avatarUrl);
}
$(slot).css('cursor', 'pointer');
container.append(slot);
count++;
}
// there are 6 slots in total,
if (count < maxCount) { //if any are left over
let leftOverSlots = maxCount - count;
for (let i = 1; i <= leftOverSlots; i++) {
container.append(template.clone());
}
} else {
//console.log(`count was ${count} so no need to knock off any selectors!`);
}
}

129
public/scripts/filters.js Normal file
View File

@@ -0,0 +1,129 @@
import { fuzzySearchCharacters, fuzzySearchGroups, power_user } from "./power-user.js";
import { tag_map } from "./tags.js";
export const FILTER_TYPES = {
SEARCH: 'search',
TAG: 'tag',
FAV: 'fav',
GROUP: 'group',
};
export class FilterHelper {
constructor(onDataChanged) {
this.onDataChanged = onDataChanged;
}
filterFunctions = {
[FILTER_TYPES.SEARCH]: this.searchFilter.bind(this),
[FILTER_TYPES.GROUP]: this.groupFilter.bind(this),
[FILTER_TYPES.FAV]: this.favFilter.bind(this),
[FILTER_TYPES.TAG]: this.tagFilter.bind(this),
}
filterData = {
[FILTER_TYPES.SEARCH]: '',
[FILTER_TYPES.GROUP]: false,
[FILTER_TYPES.FAV]: false,
[FILTER_TYPES.TAG]: { excluded: [], selected: [] },
}
tagFilter(data) {
const TAG_LOGIC_AND = true; // switch to false to use OR logic for combining tags
const { selected, excluded } = this.filterData[FILTER_TYPES.TAG];
if (!selected.length && !excluded.length) {
return data;
}
function isElementTagged(entity, tagId) {
const isCharacter = entity.type === 'character';
const lookupValue = isCharacter ? entity.item.avatar : String(entity.id);
const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId);
return isTagged;
}
function getIsTagged(entity) {
const tagFlags = selected.map(tagId => isElementTagged(entity, tagId));
const trueFlags = tagFlags.filter(x => x);
const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0;
const excludedTagFlags = excluded.map(tagId => isElementTagged(entity, tagId));
const isExcluded = excludedTagFlags.includes(true);
if (isExcluded) {
return false;
} else if (selected.length > 0 && !isTagged) {
return false;
} else {
return true;
}
}
return data.filter(entity => getIsTagged(entity));
}
favFilter(data) {
if (!this.filterData[FILTER_TYPES.FAV]) {
return data;
}
return data.filter(entity => entity.item.fav || entity.item.fav == "true");
}
groupFilter(data) {
if (!this.filterData[FILTER_TYPES.GROUP]) {
return data;
}
return data.filter(entity => entity.type === 'group');
}
searchFilter(data) {
if (!this.filterData[FILTER_TYPES.SEARCH]) {
return data;
}
const searchValue = this.filterData[FILTER_TYPES.SEARCH].trim().toLowerCase();
const fuzzySearchCharactersResults = power_user.fuzzy_search ? fuzzySearchCharacters(searchValue) : [];
const fuzzySearchGroupsResults = power_user.fuzzy_search ? fuzzySearchGroups(searchValue) : [];
function getIsValidSearch(entity) {
const isGroup = entity.type === 'group';
const isCharacter = entity.type === 'character';
if (power_user.fuzzy_search) {
if (isCharacter) {
return fuzzySearchCharactersResults.includes(parseInt(entity.id));
} else if (isGroup) {
return fuzzySearchGroupsResults.includes(String(entity.id));
} else {
return false;
}
}
else {
return entity.item?.name?.toLowerCase()?.includes(searchValue) || false;
}
}
return data.filter(entity => getIsValidSearch(entity));
}
setFilterData(filterType, data) {
const oldData = this.filterData[filterType];
this.filterData[filterType] = data;
// only trigger a data change if the data actually changed
if (JSON.stringify(oldData) !== JSON.stringify(data)) {
this.onDataChanged();
}
}
getFilterData(filterType) {
return this.filterData[filterType];
}
applyFilters(data) {
return Object.values(this.filterFunctions)
.reduce((data, fn) => fn(data), data);
}
}

View File

@@ -8,7 +8,7 @@ import {
extractAllWords,
} from './utils.js';
import { RA_CountCharTokens, humanizedDateTime, dragElement } from "./RossAscends-mods.js";
import { sortCharactersList, sortGroupMembers, loadMovingUIState } from './power-user.js';
import { sortGroupMembers, loadMovingUIState } from './power-user.js';
import {
chat,
@@ -63,6 +63,7 @@ import {
getCropPopup,
} from "../script.js";
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
import { FilterHelper } from './filters.js';
export {
selected_group,
@@ -75,7 +76,6 @@ export {
deleteGroup,
getGroupAvatar,
getGroups,
printGroups,
regenerateGroup,
resetSelectedGroup,
select_group_chats,
@@ -94,9 +94,15 @@ export const group_activation_strategy = {
LIST: 1,
};
export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, 100));
const groupAutoModeInterval = setInterval(groupChatAutoModeWorker, 5000);
const saveGroupDebounced = debounce(async (group) => await _save(group), 500);
function printGroupCandidates(fullRefresh = false) {
toastr.info('Group candidates tags filter is temporarily unavailable.');
console.log('TODO: implement printGroupCandidates');
}
async function _save(group, reload = true) {
await fetch("/editgroup", {
method: "POST",
@@ -228,7 +234,6 @@ async function saveGroupChat(groupId, shouldSaveGroup) {
if (shouldSaveGroup && response.ok) {
await editGroup(groupId);
}
sortCharactersList();
}
export async function renameGroupMember(oldAvatar, newAvatar, newName) {
@@ -330,8 +335,7 @@ async function getGroups() {
}
}
function printGroups() {
for (let group of groups) {
export function getGroupBlock(group) {
const template = $("#group_list_template .group_select").clone();
template.data("id", group.id);
template.attr("grid", group.id);
@@ -345,10 +349,14 @@ function printGroups() {
const tagsElement = template.find('.tags');
tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
$("#rm_print_characters_block").prepend(template);
updateGroupAvatar(group);
}
const avatar = getGroupAvatar(group);
if (avatar) {
$(template).find(".avatar").replaceWith(avatar);
}
return template;
}
function updateGroupAvatar(group) {
$("#rm_print_characters_block .group_select").each(function () {
if ($(this).data("id") == group.id) {
@@ -1353,7 +1361,6 @@ export async function openGroupChat(groupId, chatId) {
await editGroup(groupId, true);
await getGroupChat(groupId);
sortCharactersList();
}
export async function renameGroupChat(groupId, oldChatId, newChatId) {

1190
public/scripts/pagination.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ import {
reloadCurrentChat,
getRequestHeaders,
substituteParams,
updateVisibleDivs,
eventSource,
event_types,
getCurrentChatId,
@@ -18,7 +17,7 @@ import {
setCharacterId,
setEditedMessageId
} from "../script.js";
import { favsToHotswap, isMobile, initMovingUI } from "./RossAscends-mods.js";
import { isMobile, initMovingUI } from "./RossAscends-mods.js";
import {
groups,
resetSelectedGroup,
@@ -35,7 +34,7 @@ export {
collapseNewlines,
playMessageSound,
sortGroupMembers,
sortCharactersList,
sortEntitiesList,
fixMarkdown,
power_user,
pygmalion_options,
@@ -803,7 +802,6 @@ function loadPowerUserSettings(settings, data) {
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop("selected", true);
sortCharactersList();
reloadMarkdownProcessor(power_user.render_formulas);
loadInstructMode();
loadContextSettings();
@@ -812,7 +810,6 @@ function loadPowerUserSettings(settings, data) {
switchSpoilerMode();
loadMovingUIState();
loadCharListState();
}
async function loadCharListState() {
@@ -1129,32 +1126,12 @@ const compareFunc = (first, second) => {
}
};
function sortCharactersList() {
const arr1 = groups.map(x => ({
item: x,
id: x.id,
selector: '.group_select',
attribute: 'grid',
}))
const arr2 = characters.map((x, index) => ({
item: x,
id: index,
selector: '.character_select',
attribute: 'chid',
}));
const array = [...arr1, ...arr2];
if (power_user.sort_field == undefined || array.length === 0) {
function sortEntitiesList(entities) {
if (power_user.sort_field == undefined || entities.length === 0) {
return;
}
let orderedList = array.slice().sort((a, b) => sortFunc(a.item, b.item));
for (const item of array) {
$(`${item.selector}[${item.attribute}="${item.id}"]`).css({ 'order': orderedList.indexOf(item) });
}
updateVisibleDivs('#rm_print_characters_block', true);
entities.sort((a, b) => sortFunc(a.item, b.item));
}
function sortGroupMembers(selector) {
@@ -1900,6 +1877,7 @@ $(document).ready(() => {
power_user.never_resize_avatars = !!$(this).prop('checked');
saveSettingsDebounced();
});
$("#show_card_avatar_urls").on('input', function () {
power_user.show_card_avatar_urls = !!$(this).prop('checked');
printCharacters();
@@ -1925,8 +1903,7 @@ $(document).ready(() => {
power_user.sort_field = $(this).find(":selected").data('field');
power_user.sort_order = $(this).find(":selected").data('order');
power_user.sort_rule = $(this).find(":selected").data('rule');
sortCharactersList();
favsToHotswap();
printCharacters();
saveSettingsDebounced();
});
@@ -2038,12 +2015,6 @@ $(document).ready(() => {
saveSettingsDebounced();
});
/* $("#removeXML").on("input", function () {
power_user.removeXML = !!$(this).prop('checked');
reloadCurrentChat();
saveSettingsDebounced();
}); */
$("#token_padding").on("input", function () {
power_user.token_padding = Number($(this).val());
saveSettingsDebounced();

View File

@@ -4,12 +4,12 @@ import {
this_chid,
callPopup,
menu_type,
updateVisibleDivs,
getCharacters,
updateCharacterCount,
entitiesFilter,
} from "../script.js";
import { FILTER_TYPES } from "./filters.js";
import { selected_group } from "./group-chats.js";
import { groupCandidatesFilter, selected_group } from "./group-chats.js";
import { uuidv4 } from "./utils.js";
export {
@@ -17,7 +17,6 @@ export {
tag_map,
loadTagsSettings,
printTagFilters,
isElementTagged,
getTagsList,
appendTagToList,
createTagMapFromList,
@@ -26,18 +25,11 @@ export {
};
const random_id = () => uuidv4();
const TAG_LOGIC_AND = true; // switch to false to use OR logic for combining tags
const CHARACTER_SELECTOR = '#rm_print_characters_block > div';
const GROUP_MEMBER_SELECTOR = '#rm_group_add_members > div';
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
function getCharacterSelector(listSelector) {
if ($(listSelector).is(GROUP_FILTER_SELECTOR)) {
return GROUP_MEMBER_SELECTOR;
}
return CHARACTER_SELECTOR;
function getFilterHelper(listSelector) {
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
}
export const tag_filter_types = {
@@ -68,37 +60,20 @@ const DEFAULT_TAGS = [
let tags = [];
let tag_map = {};
function applyFavFilter(characterSelector) {
function applyFavFilter(filterHelper) {
const isSelected = $(this).hasClass('selected');
const displayFavoritesOnly = !isSelected;
$(this).toggleClass('selected', displayFavoritesOnly);
$(characterSelector).removeClass('hiddenByFav');
$(characterSelector).each(function () {
if (displayFavoritesOnly) {
if ($(this).find(".ch_fav").length !== 0) {
const shouldBeDisplayed = $(this).find(".ch_fav").val().toLowerCase().includes(true);
$(this).toggleClass('hiddenByFav', !shouldBeDisplayed);
}
}
});
updateCharacterCount(characterSelector);
updateVisibleDivs('#rm_print_characters_block', true);
filterHelper.setFilterData(FILTER_TYPES.FAV, displayFavoritesOnly);
}
function filterByGroups(characterSelector) {
function filterByGroups(filterHelper) {
const isSelected = $(this).hasClass('selected');
const displayGroupsOnly = !isSelected;
$(this).toggleClass('selected', displayGroupsOnly);
$(characterSelector).removeClass('hiddenByGroup');
$(characterSelector).each((_, element) => {
$(element).toggleClass('hiddenByGroup', displayGroupsOnly && !$(element).hasClass('group_select'));
});
updateCharacterCount(characterSelector);
updateVisibleDivs('#rm_print_characters_block', true);
filterHelper.setFilterData(FILTER_TYPES.GROUP, displayGroupsOnly);
}
function loadTagsSettings(settings) {
@@ -291,7 +266,6 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
return;
}
const characterSelector = getCharacterSelector($(listElement));
let tagElement = $('#tag_template .tag').clone();
tagElement.attr('id', tag.id);
@@ -316,11 +290,12 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
}
if (selectable) {
tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement, characterSelector));
tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement));
}
if (action) {
tagElement.on('click', () => action.bind(tagElement)(characterSelector));
const filter = getFilterHelper($(listElement));
tagElement.on('click', () => action.bind(tagElement)(filter));
tagElement.addClass('actionable');
}
if (action && tag.id === 2) {
@@ -330,7 +305,7 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
$(listElement).append(tagElement);
}
function onTagFilterClick(listElement, characterSelector) {
function onTagFilterClick(listElement) {
let excludeTag;
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
@@ -356,44 +331,10 @@ function onTagFilterClick(listElement, characterSelector) {
}
}
// TODO: Overhaul this somehow to use settings tag IDs instead
const tagIds = [...($(listElement).find(".tag.selected:not(.actionable)").map((_, el) => $(el).attr("id")))];
const excludedTagIds = [...($(listElement).find(".tag.excluded:not(.actionable)").map((_, el) => $(el).attr("id")))];
$(characterSelector).each((_, element) => applyFilterToElement(tagIds, excludedTagIds, element));
updateCharacterCount(characterSelector);
updateVisibleDivs('#rm_print_characters_block', true);
}
function applyFilterToElement(tagIds, excludedTagIds, element) {
const tagFlags = tagIds.map(tagId => isElementTagged(element, tagId));
const trueFlags = tagFlags.filter(x => x);
const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0;
const excludedTagFlags = excludedTagIds.map(tagId => isElementTagged(element, tagId));
const isExcluded = excludedTagFlags.includes(true);
if (isExcluded) {
$(element).addClass('hiddenByTag');
} else if (tagIds.length > 0 && !isTagged) {
$(element).addClass('hiddenByTag');
} else {
$(element).removeClass('hiddenByTag');
}
}
function isElementTagged(element, tagId) {
const isGroup = $(element).hasClass('group_select');
const isCharacter = $(element).hasClass('character_select') || $(element).hasClass('group_member');
const idAttr = isGroup ? 'grid' : 'chid';
const elementId = $(element).attr(idAttr);
const lookupValue = isCharacter ? characters[elementId].avatar : elementId;
const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId);
return isTagged;
}
function clearTagsFilter(characterSelector) {
$('.rm_tag_filter .tag').removeClass('selected');
$(characterSelector).removeClass('hiddenByTag');
const filterHelper = getFilterHelper($(listElement));
filterHelper.setFilterData(FILTER_TYPES.TAG, { excluded: excludedTagIds, selected: tagIds });
}
function printTagFilters(type = tag_filter_types.character) {