diff --git a/src/components/layout/AppHeader.tsx b/src/components/layout/AppHeader.tsx index b347832..09c7951 100644 --- a/src/components/layout/AppHeader.tsx +++ b/src/components/layout/AppHeader.tsx @@ -46,7 +46,7 @@ export const AppHeader: FC = ({ onChange }) => { Protontweaks
- +
diff --git a/src/service/protontweaks.service.ts b/src/service/protontweaks.service.ts index f9d4fc1..a1f68ff 100644 --- a/src/service/protontweaks.service.ts +++ b/src/service/protontweaks.service.ts @@ -26,7 +26,7 @@ export async function getApp(id: string) { return getComputedApp(await fetch(`https://api.protontweaks.com/v4/${id}.json`)); } -export function getComputedApp(app: T): ComputedApp { +export function getComputedApp>(app: T): ComputedApp { return { ...app, image_url: `https://steamcdn-a.akamaihd.net/steam/apps/${app.id}/header.jpg`, diff --git a/src/types/apps.ts b/src/types/apps.ts index c4dadb3..1499650 100644 --- a/src/types/apps.ts +++ b/src/types/apps.ts @@ -7,7 +7,14 @@ export type AppsList = ApiInfo & { apps: ThinApp[]; }; -export type ThinApp = Pick; +export type ThinApp = Pick & { + has: { + args: boolean; + env: boolean; + settings: boolean; + tricks: boolean; + }; +}; export type App = { id: string; @@ -29,7 +36,7 @@ export type App = { updated_at: string; }; -export type ComputedApp = T & { +export type ComputedApp = App> = T & { image_url: string; badges: { is_new: boolean; diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 0000000..4a8e0b8 --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,12 @@ +export function every>( + object: Partial>, + predicate: ([key, value]: [K, V | undefined]) => boolean +): boolean { + for (const key in object) { + if (!predicate([key, object[key]])) { + return false; + } + } + + return true; +} diff --git a/src/workers/search.worker.ts b/src/workers/search.worker.ts index f6ea543..fcc7413 100644 --- a/src/workers/search.worker.ts +++ b/src/workers/search.worker.ts @@ -1,4 +1,6 @@ import { getApps } from '@/service/protontweaks.service'; +import type { App, ThinApp } from '@/types'; +import { every } from '@/utils/object'; import { delay } from '@ribbon-studios/js-utils'; self.onmessage = async (event) => { @@ -15,21 +17,62 @@ self.onmessage = async (event) => { } }; +type ParsedSearchOptions = { + raw: string; + query: string; + partials: string[]; + has: Partial>; +}; + +function parseOptions(value: string): ParsedSearchOptions { + const has_filters = Array.from(value.matchAll(/has:([^\s]+)/g)); + + const has = has_filters.reduce((output, [, flag]) => { + if (['trick', 'tricks'].includes(flag)) { + output.tricks = true; + } else if (['env', 'envs'].includes(flag)) { + output.env = true; + } else if (['setting', 'settings'].includes(flag)) { + output.settings = true; + } else if (['arg', 'args'].includes(flag)) { + output.args = true; + } + + return output; + }, {}); + + const query = value + .replace(/has:[^\s]+/g, '') + .replace(/\s\s/g, ' ') + .trim(); + + return { + raw: value, + query, + partials: query.split(' ').filter(Boolean), + has, + }; +} + async function filterApps(value: string) { // TODO: Add a method of getting an api version (git sha) and using it to enable caching and cache busting const apps = await getApps(); if (!value) return apps; - const partials = value.split(' ').filter(Boolean); + const options = parseOptions(value); return apps .filter((app) => { - return partials.some((partial) => app.name.toLowerCase().includes(partial)); + return ( + (options.partials.length === 0 || + options.partials.some((partial) => app.name.toLowerCase().includes(partial))) && + every(options.has, ([key, value]) => app.has[key] === value) + ); }) .map((app) => ({ ...app, - strength: determineMatchStrength(value, partials, app.name.toLowerCase()), + strength: determineMatchStrength(value, options.partials, app.name.toLowerCase()), })) .sort((a, b) => b.strength - a.strength); }