diff --git a/src/components/composables/ModFiltersComposable.ts b/src/components/composables/ModFiltersComposable.ts index f6738dc6c..552eb9f3d 100644 --- a/src/components/composables/ModFiltersComposable.ts +++ b/src/components/composables/ModFiltersComposable.ts @@ -5,7 +5,7 @@ import SortingStyle from '../../model/enums/SortingStyle'; import ThunderstoreMod from '../../model/ThunderstoreMod'; import { getStore } from '../../providers/generic/store/StoreProvider'; import { State } from '../../store'; -import SearchUtils from '../../utils/SearchUtils'; +import SearchUtils, { ExactSearchMatchRank } from '../../utils/SearchUtils'; const store = getStore(); @@ -15,7 +15,7 @@ const filteredMods = ref([]); const filteredModCount = ref(0); function runFilter() { - let result = sortedMods.value; + let result = sortedMods.value as ThunderstoreMod[]; const searchKeys = SearchUtils.makeKeys(searchFilter.value); if (searchKeys.length > 0) { @@ -44,10 +44,53 @@ function runFilter() { result = result.filter(x => filterAll.every(c => x.getCategories().includes(c))); } + if (store.state.modFilters.sortBehaviour === SortingStyle.RELEVANCE) { + result = bumpExactMatches(result, searchFilter.value.trim().toLowerCase()); + } + filteredMods.value = result; filteredModCount.value = result.length; } +function bumpExactMatches(mods: ThunderstoreMod[], query: string): ThunderstoreMod[] { + if (query.length === 0) { + return mods; + } + + const nameMatches: ThunderstoreMod[] = []; + const authorMatches: ThunderstoreMod[] = []; + const softNameMatches: ThunderstoreMod[] = []; + const softAuthorMatches: ThunderstoreMod[] = []; + const others: ThunderstoreMod[] = []; + + for (const mod of mods) { + const rank = SearchUtils.getExactMatchRank(query, mod.getName(), mod.getFullName(), mod.getOwner()); + switch (rank) { + case ExactSearchMatchRank.NAME: + nameMatches.push(mod); + break; + case ExactSearchMatchRank.AUTHOR: + authorMatches.push(mod); + break; + case ExactSearchMatchRank.SOFT_NAME: + softNameMatches.push(mod); + break; + case ExactSearchMatchRank.SOFT_AUTHOR: + softAuthorMatches.push(mod); + break; + default: + others.push(mod); + break; + } + } + + const relevanceMatches = [...nameMatches, ...authorMatches, ...softNameMatches, ...softAuthorMatches]; + if (relevanceMatches.length === 0) { + return mods; + } + return [...relevanceMatches, ...others]; +} + function runSort() { const sortDescending = store.state.modFilters.sortDirection === SortDirection.STANDARD; const sorted = [...store.state.tsMods.mods]; @@ -66,7 +109,7 @@ function runSort() { case SortingStyle.RATING: result = a.getRating() < b.getRating(); break; - default: + default: // case SortingStyle.RELEVANCE result = true; break; } diff --git a/src/model/enums/SortingStyle.ts b/src/model/enums/SortingStyle.ts index a4e1b1a09..40d2e6b82 100644 --- a/src/model/enums/SortingStyle.ts +++ b/src/model/enums/SortingStyle.ts @@ -1,7 +1,7 @@ export default { - DEFAULT: 'Default', + RELEVANCE: 'Relevance', LAST_UPDATED: 'Last updated', ALPHABETICAL: 'Alphabetical', DOWNLOADS: 'Download count', RATING: 'Rating', -}; \ No newline at end of file +}; diff --git a/src/store/modules/ModFilterModule.ts b/src/store/modules/ModFilterModule.ts index b8d4d7d80..dc33cfd00 100644 --- a/src/store/modules/ModFilterModule.ts +++ b/src/store/modules/ModFilterModule.ts @@ -27,7 +27,7 @@ export default { selectedCategoriesToExclude: [], showDeprecatedPackages: false, sortDirection: SortDirection.STANDARD, - sortBehaviour: SortingStyle.DEFAULT + sortBehaviour: SortingStyle.RELEVANCE }), getters: >{ @@ -49,7 +49,7 @@ export default { state.selectedCategoriesCompareAll = []; state.selectedCategoriesToExclude = []; state.sortDirection = SortDirection.STANDARD; - state.sortBehaviour = SortingStyle.DEFAULT; + state.sortBehaviour = SortingStyle.RELEVANCE; }, selectCategoryToCompareOne: function(state: State, category: string) { diff --git a/src/utils/SearchUtils.ts b/src/utils/SearchUtils.ts index c47541048..fe2d17a58 100644 --- a/src/utils/SearchUtils.ts +++ b/src/utils/SearchUtils.ts @@ -1,10 +1,21 @@ +export enum ExactSearchMatchRank { + NONE = 0, + AUTHOR = 1, + NAME = 2, + SOFT_AUTHOR = 3, + SOFT_NAME = 4, +} + +const softMatcher = new RegExp(' |_', 'g'); + export default class SearchUtils { public static makeKeys(search: string) { - return search.trim().toLowerCase().split(' '); + console.log(search.trim().toLowerCase().split(softMatcher)); + return search.trim().toLowerCase().split(softMatcher); } public static isSearched(keys: string[], name: string, description?: string) { - name = name.toLowerCase(); + name = name.replaceAll(softMatcher, '').toLowerCase(); if (description) { description = description.toLowerCase(); } else { @@ -12,4 +23,31 @@ export default class SearchUtils { } return keys.every(i => name.indexOf(i) >= 0 || description.indexOf(i) >= 0); } + + public static getExactMatchRank(query: string, name: string, fullName: string, author: string): ExactSearchMatchRank { + if (query.length === 0) { + return ExactSearchMatchRank.NONE; + } + if (name.toLowerCase() === query || fullName.toLowerCase() === query) { + return ExactSearchMatchRank.NAME; + } + if (author.toLowerCase() === query) { + return ExactSearchMatchRank.AUTHOR; + } + + const softQuery = query.replaceAll(softMatcher, '').toLowerCase(); + const softName = name.replaceAll(softMatcher, ''); + const softFullName = fullName.replaceAll(softMatcher, ''); + const softAuthor = author.replaceAll(softMatcher, ''); + + if (softName.toLowerCase() === softQuery || softFullName.toLowerCase() === softQuery) { + console.log('Soft name match'); + return ExactSearchMatchRank.SOFT_NAME; + } + if (softAuthor.toLowerCase() === softQuery) { + return ExactSearchMatchRank.SOFT_AUTHOR; + } + + return ExactSearchMatchRank.NONE; + } }