diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index d2665928..ed8c7d4e 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -96,7 +96,7 @@ "search_library": "Search library", "recent_searches": "Recent Searches", "suggestions": "Suggestions", - "clear_history": "Clear history", + "clear_history": "Clear", "remove_from_history": "Remove from history", "loading": "Loading...", "no_results": "No results", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 5a65d3cf..4a582ef8 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -93,8 +93,16 @@ }, "header": { "search": "Buscar juegos", + "search_library": "Buscar en la librería", + "recent_searches": "Búsquedas Recientes", + "suggestions": "Sugerencias", + "clear_history": "Limpiar", + "remove_from_history": "Eliminar del historial", + "loading": "Cargando...", + "no_results": "Sin resultados", "home": "Inicio", "catalogue": "Catálogo", + "library": "Librería", "downloads": "Descargas", "search_results": "Resultados de búsqueda", "settings": "Ajustes", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 73d5e8fb..6702c310 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -93,11 +93,19 @@ }, "header": { "search": "Buscar jogos", + "search_library": "Buscar na biblioteca", + "recent_searches": "Buscas Recentes", + "suggestions": "Sugestões", + "clear_history": "Limpar", + "remove_from_history": "Remover do histórico", + "loading": "Carregando...", + "no_results": "Sem resultados", + "home": "Início", "catalogue": "Catálogo", + "library": "Biblioteca", "downloads": "Downloads", "search_results": "Resultados da busca", "settings": "Ajustes", - "home": "Início", "version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.", "version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download." }, diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index c8e4586d..e48e1458 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -30,11 +30,19 @@ }, "header": { "search": "Procurar jogos", + "search_library": "Procurar na biblioteca", + "recent_searches": "Pesquisas Recentes", + "suggestions": "Sugestões", + "clear_history": "Limpar", + "remove_from_history": "Remover do histórico", + "loading": "A carregar...", + "no_results": "Sem resultados", + "home": "Início", "catalogue": "Catálogo", + "library": "Biblioteca", "downloads": "Transferências", "search_results": "Resultados da pesquisa", "settings": "Definições", - "home": "Início", "version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.", "version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download." }, diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index b831ff2e..1cf7ae2f 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -93,8 +93,16 @@ }, "header": { "search": "Поиск", + "search_library": "Поиск в библиотеке", + "recent_searches": "Недавние поиски", + "suggestions": "Предложения", + "clear_history": "Очистить", + "remove_from_history": "Удалить из истории", + "loading": "Загрузка...", + "no_results": "Нет результатов", "home": "Главная", "catalogue": "Каталог", + "library": "Библиотека", "downloads": "Загрузки", "search_results": "Результаты поиска", "settings": "Настройки", diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 5f2c1d1d..1cac834c 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -15,6 +15,8 @@ import { AutoUpdateSubHeader } from "./auto-update-sub-header"; import { setFilters, setLibrarySearchQuery } from "@renderer/features"; import cn from "classnames"; import { SearchDropdown } from "@renderer/components"; +import { buildGameDetailsPath } from "@renderer/helpers"; +import type { GameShop } from "@types"; const pathTitle: Record = { "/": "home", @@ -161,11 +163,11 @@ export function Header() { const handleSelectSuggestion = (suggestion: { title: string; objectId: string; - shop: string; + shop: GameShop; }) => { setIsDropdownVisible(false); inputRef.current?.blur(); - navigate(`/game/${suggestion.shop}/${suggestion.objectId}`); + navigate(buildGameDetailsPath(suggestion)); }; const handleClearSearch = () => { diff --git a/src/renderer/src/components/search-dropdown/highlight-text.tsx b/src/renderer/src/components/search-dropdown/highlight-text.tsx index 0d5f0fe4..cbcd917d 100644 --- a/src/renderer/src/components/search-dropdown/highlight-text.tsx +++ b/src/renderer/src/components/search-dropdown/highlight-text.tsx @@ -19,24 +19,25 @@ export function HighlightText({ text, query }: Readonly) { return <>{text}; } - const textWords = text.split(/\b/); - const matches: { start: number; end: number; text: string }[] = []; + const matches: { start: number; end: number }[] = []; + const textLower = text.toLowerCase(); - let currentIndex = 0; - textWords.forEach((word) => { - const wordLower = word.toLowerCase(); + queryWords.forEach((queryWord) => { + const escapedQuery = queryWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp( + `(?:^|[\\s])${escapedQuery}(?=[\\s]|$)|^${escapedQuery}$`, + "gi" + ); - queryWords.forEach((queryWord) => { - if (wordLower === queryWord) { - matches.push({ - start: currentIndex, - end: currentIndex + word.length, - text: word, - }); - } - }); + let match; + while ((match = regex.exec(textLower)) !== null) { + const matchedText = match[0]; + const leadingSpace = matchedText.startsWith(" ") ? 1 : 0; + const start = match.index + leadingSpace; + const end = start + queryWord.length; - currentIndex += word.length; + matches.push({ start, end }); + } }); if (matches.length === 0) { @@ -46,16 +47,14 @@ export function HighlightText({ text, query }: Readonly) { matches.sort((a, b) => a.start - b.start); const mergedMatches: { start: number; end: number }[] = []; - - if (matches.length === 0) { - return <>{text}; - } - let current = matches[0]; for (let i = 1; i < matches.length; i++) { if (matches[i].start <= current.end) { - current.end = Math.max(current.end, matches[i].end); + current = { + start: current.start, + end: Math.max(current.end, matches[i].end), + }; } else { mergedMatches.push(current); current = matches[i]; diff --git a/src/renderer/src/components/search-dropdown/search-dropdown.scss b/src/renderer/src/components/search-dropdown/search-dropdown.scss index d09b3663..40a55432 100644 --- a/src/renderer/src/components/search-dropdown/search-dropdown.scss +++ b/src/renderer/src/components/search-dropdown/search-dropdown.scss @@ -24,7 +24,8 @@ display: flex; align-items: center; justify-content: space-between; - padding: 8px 12px 4px; + padding: 8px 12px 8px; + margin-bottom: 4px; } &__section-title { @@ -35,19 +36,19 @@ letter-spacing: 0.5px; } - &__clear-button { + &__clear-text-button { color: globals.$muted-color; cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: all ease 0.2s; - display: flex; - align-items: center; - justify-content: center; + padding: 0; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + transition: color ease 0.2s; + background: transparent; + border: none; &:hover { color: #ffffff; - background-color: rgba(255, 255, 255, 0.15); } } @@ -74,9 +75,8 @@ transform: translateY(-50%); color: globals.$muted-color; padding: 4px; - border-radius: 4px; opacity: 0; - transition: all ease 0.15s; + transition: opacity ease 0.15s; display: flex; align-items: center; justify-content: center; diff --git a/src/renderer/src/components/search-dropdown/search-dropdown.tsx b/src/renderer/src/components/search-dropdown/search-dropdown.tsx index d90c3bf5..4142e4a5 100644 --- a/src/renderer/src/components/search-dropdown/search-dropdown.tsx +++ b/src/renderer/src/components/search-dropdown/search-dropdown.tsx @@ -1,11 +1,6 @@ import { useEffect, useRef, useCallback, useState } from "react"; import { createPortal } from "react-dom"; -import { - ClockIcon, - SearchIcon, - TrashIcon, - XIcon, -} from "@primer/octicons-react"; +import { ClockIcon, SearchIcon, XIcon } from "@primer/octicons-react"; import cn from "classnames"; import { useTranslation } from "react-i18next"; import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history"; @@ -144,11 +139,10 @@ export function SearchDropdown({
    diff --git a/src/renderer/src/hooks/use-search-history.ts b/src/renderer/src/hooks/use-search-history.ts index dac6d391..e5ce3efa 100644 --- a/src/renderer/src/hooks/use-search-history.ts +++ b/src/renderer/src/hooks/use-search-history.ts @@ -1,4 +1,5 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { levelDBService } from "@renderer/services/leveldb.service"; export interface SearchHistoryEntry { query: string; @@ -6,22 +7,32 @@ export interface SearchHistoryEntry { context: "library" | "catalogue"; } -const STORAGE_KEY = "search-history"; +const LEVELDB_KEY = "searchHistory"; const MAX_HISTORY_ENTRIES = 15; export function useSearchHistory() { const [history, setHistory] = useState([]); + const isInitialized = useRef(false); useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { + const loadHistory = async () => { + if (isInitialized.current) return; + isInitialized.current = true; + try { - const parsed = JSON.parse(stored) as SearchHistoryEntry[]; - setHistory(parsed); + const data = (await levelDBService.get(LEVELDB_KEY, null, "json")) as + | SearchHistoryEntry[] + | null; + + if (data) { + setHistory(data); + } } catch { - localStorage.removeItem(STORAGE_KEY); + setHistory([]); } - } + }; + + loadHistory(); }, []); const addToHistory = useCallback( @@ -39,7 +50,7 @@ export function useSearchHistory() { (entry) => entry.query.toLowerCase() !== query.toLowerCase().trim() ); const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES); - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + levelDBService.put(LEVELDB_KEY, updated, null, "json"); return updated; }); }, @@ -49,14 +60,14 @@ export function useSearchHistory() { const removeFromHistory = useCallback((query: string) => { setHistory((prev) => { const updated = prev.filter((entry) => entry.query !== query); - localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + levelDBService.put(LEVELDB_KEY, updated, null, "json"); return updated; }); }, []); const clearHistory = useCallback(() => { setHistory([]); - localStorage.removeItem(STORAGE_KEY); + levelDBService.del(LEVELDB_KEY, null); }, []); const getRecentHistory = useCallback( diff --git a/src/renderer/src/hooks/use-search-suggestions.ts b/src/renderer/src/hooks/use-search-suggestions.ts index c4d5b188..b8986775 100644 --- a/src/renderer/src/hooks/use-search-suggestions.ts +++ b/src/renderer/src/hooks/use-search-suggestions.ts @@ -2,11 +2,12 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useAppSelector } from "./redux"; import { debounce } from "lodash-es"; import { logger } from "@renderer/logger"; +import type { GameShop } from "@types"; export interface SearchSuggestion { title: string; objectId: string; - shop: string; + shop: GameShop; iconUrl: string | null; source: "library" | "catalogue"; } @@ -20,6 +21,7 @@ export function useSearchSuggestions( const [isLoading, setIsLoading] = useState(false); const library = useAppSelector((state) => state.library.value); const abortControllerRef = useRef(null); + const cacheRef = useRef>(new Map()); const getLibrarySuggestions = useCallback( (searchQuery: string, limit: number = 3): SearchSuggestion[] => { @@ -68,6 +70,15 @@ export function useSearchSuggestions( return; } + const cacheKey = `${searchQuery.toLowerCase()}_${limit}`; + const cachedResults = cacheRef.current.get(cacheKey); + + if (cachedResults) { + setSuggestions(cachedResults); + setIsLoading(false); + return; + } + abortControllerRef.current?.abort(); const abortController = new AbortController(); abortControllerRef.current = abortController; @@ -79,7 +90,7 @@ export function useSearchSuggestions( { title: string; objectId: string; - shop: string; + shop: GameShop; iconUrl: string | null; }[] >("/catalogue/search/suggestions", { @@ -99,6 +110,7 @@ export function useSearchSuggestions( }) ); + cacheRef.current.set(cacheKey, catalogueSuggestions); setSuggestions(catalogueSuggestions); } catch (error) { if (!abortController.signal.aborted) {