mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge branch 'feat/new-catalogue' into feat/achievements-points
This commit is contained in:
commit
e53f4808d5
9 changed files with 96 additions and 32 deletions
|
@ -46,8 +46,15 @@
|
||||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
|
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Next page",
|
"search": "Filter…",
|
||||||
"previous_page": "Previous page"
|
"developers": "Developers",
|
||||||
|
"genres": "Genres",
|
||||||
|
"tags": "Tags",
|
||||||
|
"publishers": "Publishers",
|
||||||
|
"download_sources": "Download sources",
|
||||||
|
"result_count": "{{resultCount}} results",
|
||||||
|
"filter_count": "{{filterCount}} available",
|
||||||
|
"clear_filters": "Clear {{filterCount}} selected"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Open download options",
|
"open_download_options": "Open download options",
|
||||||
|
|
|
@ -46,8 +46,15 @@
|
||||||
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
|
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Siguiente página",
|
"search": "Filtrar…",
|
||||||
"previous_page": "Pagina anterior"
|
"developers": "Desarrolladores",
|
||||||
|
"genres": "Géneros",
|
||||||
|
"tags": "Marcadores",
|
||||||
|
"publishers": "Distribuidoras",
|
||||||
|
"download_sources": "Fuentes de descarga",
|
||||||
|
"result_count": "{{resultCount}} resultados",
|
||||||
|
"filter_count": "{{filterCount}} disponibles",
|
||||||
|
"clear_filters": "Limpiar {{filterCount}} seleccionados"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Ver opciones de descargas",
|
"open_download_options": "Ver opciones de descargas",
|
||||||
|
|
|
@ -284,11 +284,15 @@
|
||||||
"instructions": "Verifique a forma correta de instalar algum deles no seu distro Linux, garantindo assim a execução normal do jogo"
|
"instructions": "Verifique a forma correta de instalar algum deles no seu distro Linux, garantindo assim a execução normal do jogo"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"search": "Pesquisar…",
|
"search": "Filtrar…",
|
||||||
"developers": "Desenvolvedores",
|
"developers": "Desenvolvedores",
|
||||||
"genres": "Gêneros",
|
"genres": "Gêneros",
|
||||||
"tags": "Tags",
|
"tags": "Marcadores",
|
||||||
"download_sources": "Fontes de download"
|
"publishers": "Distribuidoras",
|
||||||
|
"download_sources": "Fontes de download",
|
||||||
|
"result_count": "{{resultCount}} resultados",
|
||||||
|
"filter_count": "{{filterCount}} disponíveis",
|
||||||
|
"clear_filters": "Limpar {{filterCount}} selecionados"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Botão de fechar"
|
"close": "Botão de fechar"
|
||||||
|
|
|
@ -46,8 +46,15 @@
|
||||||
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)"
|
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Следующая страница",
|
"search": "Фильтр…",
|
||||||
"previous_page": "Предыдущая страница"
|
"developers": "Разработчики",
|
||||||
|
"genres": "Жанры",
|
||||||
|
"tags": "Маркеры",
|
||||||
|
"publishers": "Издательства",
|
||||||
|
"download_sources": "Источники загрузки",
|
||||||
|
"result_count": "{{resultCount}} результатов",
|
||||||
|
"filter_count": "{{filterCount}} доступных",
|
||||||
|
"clear_filters": "Очистить {{filterCount}} выбранных"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Открыть источники",
|
"open_download_options": "Открыть источники",
|
||||||
|
|
|
@ -6,6 +6,8 @@ import type { CatalogueSearchPayload } from "@types";
|
||||||
export interface CatalogueSearchState {
|
export interface CatalogueSearchState {
|
||||||
filters: CatalogueSearchPayload;
|
filters: CatalogueSearchPayload;
|
||||||
page: number;
|
page: number;
|
||||||
|
steamUserTags: Record<string, Record<string, number>>;
|
||||||
|
steamGenres: Record<string, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: CatalogueSearchState = {
|
const initialState: CatalogueSearchState = {
|
||||||
|
@ -17,6 +19,8 @@ const initialState: CatalogueSearchState = {
|
||||||
genres: [],
|
genres: [],
|
||||||
developers: [],
|
developers: [],
|
||||||
},
|
},
|
||||||
|
steamUserTags: {},
|
||||||
|
steamGenres: {},
|
||||||
page: 1,
|
page: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,8 +45,23 @@ export const catalogueSearchSlice = createSlice({
|
||||||
clearPage: (state) => {
|
clearPage: (state) => {
|
||||||
state.page = initialState.page;
|
state.page = initialState.page;
|
||||||
},
|
},
|
||||||
|
setTags: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<Record<string, Record<string, number>>>
|
||||||
|
) => {
|
||||||
|
state.steamUserTags = action.payload;
|
||||||
|
},
|
||||||
|
setGenres: (state, action: PayloadAction<Record<string, string[]>>) => {
|
||||||
|
state.steamGenres = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setFilters, clearFilters, setPage, clearPage } =
|
export const {
|
||||||
catalogueSearchSlice.actions;
|
setFilters,
|
||||||
|
clearFilters,
|
||||||
|
setPage,
|
||||||
|
clearPage,
|
||||||
|
setTags,
|
||||||
|
setGenres,
|
||||||
|
} = catalogueSearchSlice.actions;
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useAppDispatch } from "./redux";
|
||||||
|
import { setGenres, setTags } from "@renderer/features";
|
||||||
|
|
||||||
export const externalResourcesInstance = axios.create({
|
export const externalResourcesInstance = axios.create({
|
||||||
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
|
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useCatalogue() {
|
export function useCatalogue() {
|
||||||
const [steamGenres, setSteamGenres] = useState<Record<string, string[]>>({});
|
const dispatch = useAppDispatch();
|
||||||
const [steamUserTags, setSteamUserTags] = useState<
|
|
||||||
Record<string, Record<string, number>>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const [steamPublishers, setSteamPublishers] = useState<string[]>([]);
|
const [steamPublishers, setSteamPublishers] = useState<string[]>([]);
|
||||||
const [steamDevelopers, setSteamDevelopers] = useState<string[]>([]);
|
const [steamDevelopers, setSteamDevelopers] = useState<string[]>([]);
|
||||||
|
|
||||||
const getSteamUserTags = useCallback(() => {
|
const getSteamUserTags = useCallback(() => {
|
||||||
externalResourcesInstance.get("/steam-user-tags.json").then((response) => {
|
externalResourcesInstance.get("/steam-user-tags.json").then((response) => {
|
||||||
setSteamUserTags(response.data);
|
dispatch(setTags(response.data));
|
||||||
});
|
});
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
const getSteamGenres = useCallback(() => {
|
const getSteamGenres = useCallback(() => {
|
||||||
externalResourcesInstance.get("/steam-genres.json").then((response) => {
|
externalResourcesInstance.get("/steam-genres.json").then((response) => {
|
||||||
setSteamGenres(response.data);
|
dispatch(setGenres(response.data));
|
||||||
});
|
});
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
const getSteamPublishers = useCallback(() => {
|
const getSteamPublishers = useCallback(() => {
|
||||||
externalResourcesInstance.get("/steam-publishers.json").then((response) => {
|
externalResourcesInstance.get("/steam-publishers.json").then((response) => {
|
||||||
|
@ -50,5 +49,5 @@ export function useCatalogue() {
|
||||||
getSteamDevelopers,
|
getSteamDevelopers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { steamGenres, steamUserTags, steamPublishers, steamDevelopers };
|
return { steamPublishers, steamDevelopers };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
@use "../../scss/globals.scss";
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
.catalogue {
|
.catalogue {
|
||||||
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
&__filters-container {
|
&__filters-container {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
|
|
|
@ -33,9 +33,13 @@ const PAGE_SIZE = 20;
|
||||||
|
|
||||||
export default function Catalogue() {
|
export default function Catalogue() {
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const cataloguePageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { steamGenres, steamUserTags, steamDevelopers, steamPublishers } =
|
const { steamDevelopers, steamPublishers } = useCatalogue();
|
||||||
useCatalogue();
|
|
||||||
|
const { steamGenres, steamUserTags } = useAppSelector(
|
||||||
|
(state) => state.catalogueSearch
|
||||||
|
);
|
||||||
|
|
||||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
@ -128,7 +132,7 @@ export default function Catalogue() {
|
||||||
...filters.tags.map((tag) => ({
|
...filters.tags.map((tag) => ({
|
||||||
label: Object.keys(steamUserTags[language]).find(
|
label: Object.keys(steamUserTags[language]).find(
|
||||||
(key) => steamUserTags[language][key] === tag
|
(key) => steamUserTags[language][key] === tag
|
||||||
) as string,
|
),
|
||||||
orbColor: filterCategoryColors.tags,
|
orbColor: filterCategoryColors.tags,
|
||||||
key: "tags",
|
key: "tags",
|
||||||
value: tag,
|
value: tag,
|
||||||
|
@ -214,7 +218,7 @@ export default function Catalogue() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="catalogue">
|
<div className="catalogue" ref={cataloguePageRef}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -237,7 +241,7 @@ export default function Catalogue() {
|
||||||
{groupedFilters.map((filter) => (
|
{groupedFilters.map((filter) => (
|
||||||
<li key={`${filter.key}-${filter.value}`}>
|
<li key={`${filter.key}-${filter.value}`}>
|
||||||
<FilterItem
|
<FilterItem
|
||||||
filter={filter.label}
|
filter={filter.label ?? ""}
|
||||||
orbColor={filter.orbColor}
|
orbColor={filter.orbColor}
|
||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -298,12 +302,21 @@ export default function Catalogue() {
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{formatNumber(itemsCount)} resultados</span>
|
<span style={{ fontSize: 12 }}>
|
||||||
|
{t("result_count", {
|
||||||
|
resultCount: formatNumber(itemsCount),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
page={page}
|
page={page}
|
||||||
totalPages={Math.ceil(itemsCount / PAGE_SIZE)}
|
totalPages={Math.ceil(itemsCount / PAGE_SIZE)}
|
||||||
onPageChange={(page) => dispatch(setPage(page))}
|
onPageChange={(page) => {
|
||||||
|
dispatch(setPage(page));
|
||||||
|
if (cataloguePageRef.current) {
|
||||||
|
cataloguePageRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { useFormat } from "@renderer/hooks";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
import List from "rc-virtual-list";
|
import List from "rc-virtual-list";
|
||||||
|
import { vars } from "@renderer/theme.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface FilterSectionProps {
|
export interface FilterSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -24,6 +26,7 @@ export function FilterSection({
|
||||||
onClear,
|
onClear,
|
||||||
}: FilterSectionProps) {
|
}: FilterSectionProps) {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const { t } = useTranslation("catalogue");
|
||||||
|
|
||||||
const filteredItems = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
if (search.length > 0) {
|
if (search.length > 0) {
|
||||||
|
@ -64,7 +67,6 @@ export function FilterSection({
|
||||||
style={{
|
style={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: "#fff",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
@ -78,22 +80,26 @@ export function FilterSection({
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
display: "block",
|
display: "block",
|
||||||
color: "#fff",
|
color: vars.color.body,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textDecoration: "underline",
|
textDecoration: "underline",
|
||||||
}}
|
}}
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Limpar {formatNumber(selectedItemsCount)} selecionados
|
{t("clear_filters", {
|
||||||
|
filterCount: formatNumber(selectedItemsCount),
|
||||||
|
})}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ fontSize: 12, marginBottom: 12, display: "block" }}>
|
<span style={{ fontSize: 12, marginBottom: 12, display: "block" }}>
|
||||||
{formatNumber(items.length)} disponíveis
|
{t("filter_count", {
|
||||||
|
filterCount: formatNumber(items.length),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Search..."
|
placeholder={t("search")}
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
value={search}
|
value={search}
|
||||||
containerProps={{ style: { marginBottom: 16 } }}
|
containerProps={{ style: { marginBottom: 16 } }}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue