diff options
Diffstat (limited to 'src/lib/search')
| -rw-r--r-- | src/lib/search/debounce.ts | 30 | ||||
| -rw-r--r-- | src/lib/search/meilisearch.ts | 125 | ||||
| -rw-r--r-- | src/lib/search/types.ts | 27 | ||||
| -rw-r--r-- | src/lib/search/utils.ts | 17 |
4 files changed, 199 insertions, 0 deletions
diff --git a/src/lib/search/debounce.ts b/src/lib/search/debounce.ts new file mode 100644 index 0000000..4b685e7 --- /dev/null +++ b/src/lib/search/debounce.ts @@ -0,0 +1,30 @@ +/** + * Creates a debounced version of a function that delays execution until after + * the specified wait time has elapsed since the last invocation. + */ +export function debounce<T extends (...args: any[]) => any>( + func: T, + wait: number +): (...args: Parameters<T>) => void { + let timeoutId: ReturnType<typeof setTimeout> | null = null; + + return function (...args: Parameters<T>) { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(() => { + func(...args); + timeoutId = null; + }, wait); + }; +} + +/** + * Clears a timeout if it exists + */ +export function clearDebounce(timeoutId: ReturnType<typeof setTimeout> | null) { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } +} diff --git a/src/lib/search/meilisearch.ts b/src/lib/search/meilisearch.ts new file mode 100644 index 0000000..9b4c0d1 --- /dev/null +++ b/src/lib/search/meilisearch.ts @@ -0,0 +1,125 @@ +export interface MeilisearchConfig { + host: string; + apiKey?: string; +} + +export interface SearchFilters { + query?: string; + source?: string; + category?: string; + lang?: string; + nsfw?: boolean; + page?: number; + limit?: number; +} + +interface MeilisearchClient { + host: string; + apiKey: string; +} + +let client: MeilisearchClient | null = null; + +export function initMeilisearch(config: MeilisearchConfig) { + if (!config.host) { + console.warn('Meilisearch not configured'); + return null; + } + client = { host: config.host, apiKey: config.apiKey ?? '' }; + return client; +} + +export function isMeilisearchEnabled(): boolean { + return client !== null; +} + +/** + * Transforms a Meilisearch hit to EnrichedExtension format + */ +export function transformMeilisearchHit(hit: any) { + return { + name: hit.name, + pkg: hit.pkg, + apk: hit.apk, + lang: hit.lang, + code: hit.code, + version: hit.version, + nsfw: hit.nsfw, + repoUrl: hit.repoUrl, + sourceName: hit.sourceName, + formattedSourceName: hit.formattedSourceName + }; +} + +export async function searchExtensions(filters: SearchFilters) { + if (!client) { + throw new Error('Meilisearch client not initialized'); + } + + const filterConditions: string[] = []; + + if (filters.source && filters.source !== 'all') + filterConditions.push(`formattedSourceName = "${filters.source}"`); + if (filters.category && filters.category !== 'all') + filterConditions.push(`category = "${filters.category}"`); + if (filters.lang && filters.lang !== 'all') filterConditions.push(`lang = "${filters.lang}"`); + if (filters.nsfw === false) filterConditions.push('nsfw = 0'); + + const page = filters.page || 1; + const limit = filters.limit || 50; + const offset = (page - 1) * limit; + + const body: Record<string, any> = { + q: filters.query || '', + limit, + offset + }; + + if (filterConditions.length > 0) body.filter = filterConditions; + + const response = await fetch(`${client.host}/indexes/extensions/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${client.apiKey}` + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + throw new Error(`Meilisearch error: ${response.status} ${response.statusText}`); + } + + return await response.json(); +} + +export async function getFilterOptions() { + if (!client) { + throw new Error('Meilisearch client not initialized'); + } + + const response = await fetch(`${client.host}/indexes/extensions/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${client.apiKey}` + }, + body: JSON.stringify({ + q: '', + limit: 0, + facets: ['formattedSourceName', 'category', 'lang'] + }) + }); + + if (!response.ok) { + throw new Error(`Meilisearch error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + return { + sources: Object.keys(result.facetDistribution?.formattedSourceName || {}), + categories: Object.keys(result.facetDistribution?.category || {}), + languages: Object.keys(result.facetDistribution?.lang || {}) + }; +} diff --git a/src/lib/search/types.ts b/src/lib/search/types.ts new file mode 100644 index 0000000..cc30ae5 --- /dev/null +++ b/src/lib/search/types.ts @@ -0,0 +1,27 @@ +import type { Extension } from '$lib/types'; + +/** + * Extension enriched with repository and source information + */ +export interface EnrichedExtension extends Extension { + repoUrl: string; + sourceName: string; + formattedSourceName: string; + category: string; +} + +/** + * Repository information from data.json + */ +export interface RepoInfo { + name: string; + path: string; + commit: string; +} + +/** + * Repository data grouped by category + */ +export interface RepoData { + [category: string]: RepoInfo[]; +} diff --git a/src/lib/search/utils.ts b/src/lib/search/utils.ts new file mode 100644 index 0000000..d6b6aa8 --- /dev/null +++ b/src/lib/search/utils.ts @@ -0,0 +1,17 @@ +/** + * Formats a source name to lowercase with dots instead of spaces + */ +export function formatSourceName(sourceName: string): string { + return sourceName.toLowerCase().replace(/\s+/g, '.'); +} + +/** + * Finds a source by its formatted name from available sources + */ +export function findSourceByFormattedName( + formattedName: string, + availableSources: string[] +): string { + if (formattedName === 'all') return 'all'; + return availableSources.find((source) => formatSourceName(source) === formattedName) ?? 'all'; +} |
