mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Merge branch 'main' into main
This commit is contained in:
@@ -96,7 +96,7 @@
|
|||||||
"search_library": "Search library",
|
"search_library": "Search library",
|
||||||
"recent_searches": "Recent Searches",
|
"recent_searches": "Recent Searches",
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"clear_history": "Clear history",
|
"clear_history": "Clear",
|
||||||
"remove_from_history": "Remove from history",
|
"remove_from_history": "Remove from history",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
|
|||||||
@@ -93,8 +93,16 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"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",
|
"home": "Inicio",
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
|
"library": "Librería",
|
||||||
"downloads": "Descargas",
|
"downloads": "Descargas",
|
||||||
"search_results": "Resultados de búsqueda",
|
"search_results": "Resultados de búsqueda",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
|
|||||||
@@ -93,11 +93,19 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"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",
|
"catalogue": "Catálogo",
|
||||||
|
"library": "Biblioteca",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"search_results": "Resultados da busca",
|
"search_results": "Resultados da busca",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"home": "Início",
|
|
||||||
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
"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."
|
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,11 +30,19 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Procurar jogos",
|
"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",
|
"catalogue": "Catálogo",
|
||||||
|
"library": "Biblioteca",
|
||||||
"downloads": "Transferências",
|
"downloads": "Transferências",
|
||||||
"search_results": "Resultados da pesquisa",
|
"search_results": "Resultados da pesquisa",
|
||||||
"settings": "Definições",
|
"settings": "Definições",
|
||||||
"home": "Início",
|
|
||||||
"version_available_install": "Versão {{version}} disponível. Clica aqui para reiniciar e instalar.",
|
"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."
|
"version_available_download": "Versão {{version}} disponível. Clica aqui para fazer o download."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,8 +93,16 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
|
"search_library": "Поиск в библиотеке",
|
||||||
|
"recent_searches": "Недавние поиски",
|
||||||
|
"suggestions": "Предложения",
|
||||||
|
"clear_history": "Очистить",
|
||||||
|
"remove_from_history": "Удалить из истории",
|
||||||
|
"loading": "Загрузка...",
|
||||||
|
"no_results": "Нет результатов",
|
||||||
"home": "Главная",
|
"home": "Главная",
|
||||||
"catalogue": "Каталог",
|
"catalogue": "Каталог",
|
||||||
|
"library": "Библиотека",
|
||||||
"downloads": "Загрузки",
|
"downloads": "Загрузки",
|
||||||
"search_results": "Результаты поиска",
|
"search_results": "Результаты поиска",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
|||||||
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
|
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { SearchDropdown } from "@renderer/components";
|
import { SearchDropdown } from "@renderer/components";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const pathTitle: Record<string, string> = {
|
const pathTitle: Record<string, string> = {
|
||||||
"/": "home",
|
"/": "home",
|
||||||
@@ -161,11 +163,11 @@ export function Header() {
|
|||||||
const handleSelectSuggestion = (suggestion: {
|
const handleSelectSuggestion = (suggestion: {
|
||||||
title: string;
|
title: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: string;
|
shop: GameShop;
|
||||||
}) => {
|
}) => {
|
||||||
setIsDropdownVisible(false);
|
setIsDropdownVisible(false);
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
navigate(`/game/${suggestion.shop}/${suggestion.objectId}`);
|
navigate(buildGameDetailsPath(suggestion));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearSearch = () => {
|
const handleClearSearch = () => {
|
||||||
|
|||||||
@@ -19,24 +19,25 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
|
|||||||
return <>{text}</>;
|
return <>{text}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textWords = text.split(/\b/);
|
const matches: { start: number; end: number }[] = [];
|
||||||
const matches: { start: number; end: number; text: string }[] = [];
|
const textLower = text.toLowerCase();
|
||||||
|
|
||||||
let currentIndex = 0;
|
queryWords.forEach((queryWord) => {
|
||||||
textWords.forEach((word) => {
|
const escapedQuery = queryWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const wordLower = word.toLowerCase();
|
const regex = new RegExp(
|
||||||
|
`(?:^|[\\s])${escapedQuery}(?=[\\s]|$)|^${escapedQuery}$`,
|
||||||
|
"gi"
|
||||||
|
);
|
||||||
|
|
||||||
queryWords.forEach((queryWord) => {
|
let match;
|
||||||
if (wordLower === queryWord) {
|
while ((match = regex.exec(textLower)) !== null) {
|
||||||
matches.push({
|
const matchedText = match[0];
|
||||||
start: currentIndex,
|
const leadingSpace = matchedText.startsWith(" ") ? 1 : 0;
|
||||||
end: currentIndex + word.length,
|
const start = match.index + leadingSpace;
|
||||||
text: word,
|
const end = start + queryWord.length;
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentIndex += word.length;
|
matches.push({ start, end });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matches.length === 0) {
|
if (matches.length === 0) {
|
||||||
@@ -46,16 +47,14 @@ export function HighlightText({ text, query }: Readonly<HighlightTextProps>) {
|
|||||||
matches.sort((a, b) => a.start - b.start);
|
matches.sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
const mergedMatches: { start: number; end: number }[] = [];
|
const mergedMatches: { start: number; end: number }[] = [];
|
||||||
|
|
||||||
if (matches.length === 0) {
|
|
||||||
return <>{text}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let current = matches[0];
|
let current = matches[0];
|
||||||
|
|
||||||
for (let i = 1; i < matches.length; i++) {
|
for (let i = 1; i < matches.length; i++) {
|
||||||
if (matches[i].start <= current.end) {
|
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 {
|
} else {
|
||||||
mergedMatches.push(current);
|
mergedMatches.push(current);
|
||||||
current = matches[i];
|
current = matches[i];
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 12px 4px;
|
padding: 8px 12px 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section-title {
|
&__section-title {
|
||||||
@@ -35,19 +36,19 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__clear-button {
|
&__clear-text-button {
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
border-radius: 4px;
|
font-size: 11px;
|
||||||
transition: all ease 0.2s;
|
font-weight: bold;
|
||||||
display: flex;
|
text-transform: uppercase;
|
||||||
align-items: center;
|
transition: color ease 0.2s;
|
||||||
justify-content: center;
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +75,8 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all ease 0.15s;
|
transition: opacity ease 0.15s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { useEffect, useRef, useCallback, useState } from "react";
|
import { useEffect, useRef, useCallback, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import { ClockIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
||||||
ClockIcon,
|
|
||||||
SearchIcon,
|
|
||||||
TrashIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history";
|
import type { SearchHistoryEntry } from "@renderer/hooks/use-search-history";
|
||||||
@@ -144,11 +139,10 @@ export function SearchDropdown({
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="search-dropdown__clear-button"
|
className="search-dropdown__clear-text-button"
|
||||||
onClick={onClearHistory}
|
onClick={onClearHistory}
|
||||||
title={t("clear_history")}
|
|
||||||
>
|
>
|
||||||
<TrashIcon size={14} />
|
{t("clear_history")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul className="search-dropdown__list">
|
<ul className="search-dropdown__list">
|
||||||
|
|||||||
@@ -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 {
|
export interface SearchHistoryEntry {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -6,22 +7,32 @@ export interface SearchHistoryEntry {
|
|||||||
context: "library" | "catalogue";
|
context: "library" | "catalogue";
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = "search-history";
|
const LEVELDB_KEY = "searchHistory";
|
||||||
const MAX_HISTORY_ENTRIES = 15;
|
const MAX_HISTORY_ENTRIES = 15;
|
||||||
|
|
||||||
export function useSearchHistory() {
|
export function useSearchHistory() {
|
||||||
const [history, setHistory] = useState<SearchHistoryEntry[]>([]);
|
const [history, setHistory] = useState<SearchHistoryEntry[]>([]);
|
||||||
|
const isInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const loadHistory = async () => {
|
||||||
if (stored) {
|
if (isInitialized.current) return;
|
||||||
|
isInitialized.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored) as SearchHistoryEntry[];
|
const data = (await levelDBService.get(LEVELDB_KEY, null, "json")) as
|
||||||
setHistory(parsed);
|
| SearchHistoryEntry[]
|
||||||
|
| null;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setHistory(data);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
setHistory([]);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
loadHistory();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addToHistory = useCallback(
|
const addToHistory = useCallback(
|
||||||
@@ -39,7 +50,7 @@ export function useSearchHistory() {
|
|||||||
(entry) => entry.query.toLowerCase() !== query.toLowerCase().trim()
|
(entry) => entry.query.toLowerCase() !== query.toLowerCase().trim()
|
||||||
);
|
);
|
||||||
const updated = [newEntry, ...filtered].slice(0, MAX_HISTORY_ENTRIES);
|
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;
|
return updated;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -49,14 +60,14 @@ export function useSearchHistory() {
|
|||||||
const removeFromHistory = useCallback((query: string) => {
|
const removeFromHistory = useCallback((query: string) => {
|
||||||
setHistory((prev) => {
|
setHistory((prev) => {
|
||||||
const updated = prev.filter((entry) => entry.query !== query);
|
const updated = prev.filter((entry) => entry.query !== query);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
levelDBService.put(LEVELDB_KEY, updated, null, "json");
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearHistory = useCallback(() => {
|
const clearHistory = useCallback(() => {
|
||||||
setHistory([]);
|
setHistory([]);
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
levelDBService.del(LEVELDB_KEY, null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getRecentHistory = useCallback(
|
const getRecentHistory = useCallback(
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|||||||
import { useAppSelector } from "./redux";
|
import { useAppSelector } from "./redux";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { logger } from "@renderer/logger";
|
import { logger } from "@renderer/logger";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
export interface SearchSuggestion {
|
export interface SearchSuggestion {
|
||||||
title: string;
|
title: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: string;
|
shop: GameShop;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
source: "library" | "catalogue";
|
source: "library" | "catalogue";
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ export function useSearchSuggestions(
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const library = useAppSelector((state) => state.library.value);
|
const library = useAppSelector((state) => state.library.value);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const cacheRef = useRef<Map<string, SearchSuggestion[]>>(new Map());
|
||||||
|
|
||||||
const getLibrarySuggestions = useCallback(
|
const getLibrarySuggestions = useCallback(
|
||||||
(searchQuery: string, limit: number = 3): SearchSuggestion[] => {
|
(searchQuery: string, limit: number = 3): SearchSuggestion[] => {
|
||||||
@@ -68,6 +70,15 @@ export function useSearchSuggestions(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${searchQuery.toLowerCase()}_${limit}`;
|
||||||
|
const cachedResults = cacheRef.current.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedResults) {
|
||||||
|
setSuggestions(cachedResults);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
abortControllerRef.current?.abort();
|
abortControllerRef.current?.abort();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
@@ -79,7 +90,7 @@ export function useSearchSuggestions(
|
|||||||
{
|
{
|
||||||
title: string;
|
title: string;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: string;
|
shop: GameShop;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
}[]
|
}[]
|
||||||
>("/catalogue/search/suggestions", {
|
>("/catalogue/search/suggestions", {
|
||||||
@@ -99,6 +110,7 @@ export function useSearchSuggestions(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cacheRef.current.set(cacheKey, catalogueSuggestions);
|
||||||
setSuggestions(catalogueSuggestions);
|
setSuggestions(catalogueSuggestions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
|
|||||||
Reference in New Issue
Block a user