|
|
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js'; |
|
|
import { tag_map } from './tags.js'; |
|
|
import { includesIgnoreCaseAndAccents } from './utils.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', |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const FILTER_STATES = { |
|
|
SELECTED: { key: 'SELECTED', class: 'selected' }, |
|
|
EXCLUDED: { key: 'EXCLUDED', class: 'excluded' }, |
|
|
UNDEFINED: { key: 'UNDEFINED', class: 'undefined' }, |
|
|
}; |
|
|
|
|
|
export const DEFAULT_FILTER_STATE = FILTER_STATES.UNDEFINED.key; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function isFilterState(a, b) { |
|
|
const states = Object.keys(FILTER_STATES); |
|
|
|
|
|
const aKey = typeof a == 'string' && states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a); |
|
|
const bKey = typeof b == 'string' && states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b); |
|
|
|
|
|
return aKey === bKey; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const fuzzySearchCategories = Object.freeze({ |
|
|
characters: 'characters', |
|
|
worldInfo: 'worldInfo', |
|
|
personas: 'personas', |
|
|
tags: 'tags', |
|
|
groups: 'groups', |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class FilterHelper { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scoreCache; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fuzzySearchCaches; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(onDataChanged) { |
|
|
this.onDataChanged = onDataChanged; |
|
|
this.scoreCache = new Map(); |
|
|
this.fuzzySearchCaches = { |
|
|
[fuzzySearchCategories.characters]: { resultMap: new Map() }, |
|
|
[fuzzySearchCategories.worldInfo]: { resultMap: new Map() }, |
|
|
[fuzzySearchCategories.personas]: { resultMap: new Map() }, |
|
|
[fuzzySearchCategories.tags]: { resultMap: new Map() }, |
|
|
[fuzzySearchCategories.groups]: { resultMap: new Map() }, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hasAnyFilter() { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function checkRecursive(obj) { |
|
|
if (typeof obj === 'string' && obj.length > 0 && obj !== 'UNDEFINED') { |
|
|
return true; |
|
|
} else if (typeof obj === 'boolean' && obj) { |
|
|
return true; |
|
|
} else if (Array.isArray(obj) && obj.length > 0) { |
|
|
return true; |
|
|
} else if (typeof obj === 'object' && obj !== null && Object.keys(obj.length > 0)) { |
|
|
for (const key in obj) { |
|
|
if (checkRecursive(obj[key])) { |
|
|
return true; |
|
|
} |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
return checkRecursive(this.filterData); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
filterFunctions = { |
|
|
[FILTER_TYPES.SEARCH]: this.searchFilter.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), |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
filterData = { |
|
|
[FILTER_TYPES.SEARCH]: '', |
|
|
[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]: '', |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
wiSearchFilter(data) { |
|
|
const term = this.filterData[FILTER_TYPES.WORLD_INFO_SEARCH]; |
|
|
|
|
|
if (!term) { |
|
|
return data; |
|
|
} |
|
|
|
|
|
const fuzzySearchResults = fuzzySearchWorldInfo(data, term, this.fuzzySearchCaches); |
|
|
this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score]))); |
|
|
|
|
|
const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity)); |
|
|
return filteredData; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
personaSearchFilter(data) { |
|
|
const term = this.filterData[FILTER_TYPES.PERSONA_SEARCH]; |
|
|
|
|
|
if (!term) { |
|
|
return data; |
|
|
} |
|
|
|
|
|
const fuzzySearchResults = fuzzySearchPersonas(data, term, this.fuzzySearchCaches); |
|
|
this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score]))); |
|
|
|
|
|
const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name)); |
|
|
return filteredData; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tagFilter(data) { |
|
|
const TAG_LOGIC_AND = true; |
|
|
const { selected, excluded } = this.filterData[FILTER_TYPES.TAG]; |
|
|
|
|
|
if (!selected.length && !excluded.length) { |
|
|
return data; |
|
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
const excludedTagFlags = excluded.map(tagId => this.isElementTagged(entity, tagId)); |
|
|
const isExcluded = excludedTagFlags.includes(true); |
|
|
|
|
|
if (isTag) { |
|
|
return true; |
|
|
} else if (isExcluded) { |
|
|
return false; |
|
|
} else if (selected.length > 0 && !isTagged) { |
|
|
return false; |
|
|
} else { |
|
|
return true; |
|
|
} |
|
|
}; |
|
|
|
|
|
return data.filter(entity => getIsTagged(entity)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
favFilter(data) { |
|
|
const state = this.filterData[FILTER_TYPES.FAV]; |
|
|
const isFav = entity => entity.item.fav || entity.item.fav == 'true'; |
|
|
|
|
|
return this.filterDataByState(data, state, isFav, { includeFolders: true }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
groupFilter(data) { |
|
|
const state = this.filterData[FILTER_TYPES.GROUP]; |
|
|
const isGroup = entity => entity.type === 'group'; |
|
|
|
|
|
return this.filterDataByState(data, state, isGroup, { includeFolders: true }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
folderFilter(data) { |
|
|
const state = this.filterData[FILTER_TYPES.FOLDER]; |
|
|
|
|
|
const isFolder = entity => entity.type === 'tag'; |
|
|
|
|
|
return this.filterDataByState(data, state, isFolder); |
|
|
} |
|
|
|
|
|
filterDataByState(data, state, filterFunc, { includeFolders = false } = {}) { |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
searchFilter(data) { |
|
|
if (!this.filterData[FILTER_TYPES.SEARCH]) { |
|
|
return data; |
|
|
} |
|
|
|
|
|
const searchValue = this.filterData[FILTER_TYPES.SEARCH]; |
|
|
|
|
|
|
|
|
if (power_user.fuzzy_search) { |
|
|
const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue, this.fuzzySearchCaches); |
|
|
const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue, this.fuzzySearchCaches); |
|
|
const fuzzySearchTagsResult = fuzzySearchTags(searchValue, this.fuzzySearchCaches); |
|
|
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score]))); |
|
|
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score]))); |
|
|
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score]))); |
|
|
} |
|
|
|
|
|
const _this = this; |
|
|
function getIsValidSearch(entity) { |
|
|
if (power_user.fuzzy_search) { |
|
|
|
|
|
const score = _this.getScore(FILTER_TYPES.SEARCH, `${entity.type}.${entity.id}`); |
|
|
return score !== undefined; |
|
|
} |
|
|
else { |
|
|
|
|
|
return includesIgnoreCaseAndAccents(entity.item?.name, searchValue); |
|
|
} |
|
|
} |
|
|
|
|
|
return data.filter(entity => getIsValidSearch(entity)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setFilterData(filterType, data, suppressDataChanged = false) { |
|
|
const oldData = this.filterData[filterType]; |
|
|
this.filterData[filterType] = data; |
|
|
|
|
|
|
|
|
if (JSON.stringify(oldData) !== JSON.stringify(data) && !suppressDataChanged) { |
|
|
this.onDataChanged(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getFilterData(filterType) { |
|
|
return this.filterData[filterType]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
applyFilters(data, { clearScoreCache = true, tempOverrides = {}, clearFuzzySearchCaches = true } = {}) { |
|
|
if (clearScoreCache) this.clearScoreCache(); |
|
|
|
|
|
if (clearFuzzySearchCaches) this.clearFuzzySearchCaches(); |
|
|
|
|
|
|
|
|
const originalStates = {}; |
|
|
for (const key in tempOverrides) { |
|
|
originalStates[key] = this.filterData[key]; |
|
|
this.filterData[key] = tempOverrides[key]; |
|
|
} |
|
|
|
|
|
try { |
|
|
const result = Object.values(this.filterFunctions) |
|
|
.reduce((data, fn) => fn(data), data); |
|
|
|
|
|
|
|
|
for (const key in originalStates) { |
|
|
this.filterData[key] = originalStates[key]; |
|
|
} |
|
|
|
|
|
return result; |
|
|
} catch (error) { |
|
|
|
|
|
for (const key in originalStates) { |
|
|
this.filterData[key] = originalStates[key]; |
|
|
} |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cacheScores(type, results) { |
|
|
|
|
|
const typeScores = this.scoreCache.get(type) || new Map(); |
|
|
for (const [uid, score] of results) { |
|
|
typeScores.set(uid, score); |
|
|
} |
|
|
this.scoreCache.set(type, typeScores); |
|
|
console.debug('search scores chached', type, typeScores); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getScore(type, uid) { |
|
|
return this.scoreCache.get(type)?.get(uid) ?? undefined; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearScoreCache(type) { |
|
|
if (type) { |
|
|
this.scoreCache.set(type, new Map()); |
|
|
} else { |
|
|
this.scoreCache = new Map(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearFuzzySearchCaches() { |
|
|
for (const cache of Object.values(this.fuzzySearchCaches)) { |
|
|
cache.resultMap.clear(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|