Merge branch 'main' into feat/LBX-691

This commit is contained in:
Chubby Granny Chaser
2026-05-16 17:30:57 +01:00
committed by GitHub
15 changed files with 313 additions and 77 deletions
+2 -2
View File
@@ -56,7 +56,7 @@
"@tiptap/react": "^3.6.2",
"@tiptap/starter-kit": "^3.6.2",
"auto-launch": "^5.0.6",
"axios": "^1.15.0",
"axios": "^1.15.2",
"axios-cookiejar-support": "^5.0.5",
"check-disk-space": "^3.4.0",
"classic-level": "^2.0.0",
@@ -101,7 +101,7 @@
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
"string-argv": "^0.3.2",
"sudo-prompt": "^9.2.1",
"tar": "^7.5.13",
"tar": "^7.5.14",
"tough-cookie": "^5.1.1",
"user-agents": "^1.1.387",
"uuid": "^14.0.0",
+12 -1
View File
@@ -177,7 +177,18 @@
"release_year": "Release Year",
"filter_by_release_year": "Filter by release year",
"release_year_gte": "Release year from",
"release_year_lte": "Release year to"
"release_year_lte": "Release year to",
"sort_by": "Sort by",
"sort_popularity": "Popularity",
"sort_newest": "Newest releases",
"sort_oldest": "Oldest releases",
"sort_title_asc": "Title (A-Z)",
"sort_title_desc": "Title (Z-A)",
"sort_highest_rating": "Highest rating",
"sort_lowest_rating": "Lowest rating",
"filters_sidebar_hint": "Use the sidebar to refine your search",
"active_filters": "Active filters",
"no_genres": "No categories"
},
"game_details": {
"open_download_options": "Open download options",
+12 -1
View File
@@ -173,7 +173,18 @@
"deck_verified_only": "Solo verificado",
"result_count": "{{resultCount}} resultados",
"filter_count": "{{filterCount}} disponible",
"clear_filters": "Limpiar {{filterCount}} seleccionados"
"clear_filters": "Limpiar {{filterCount}} seleccionados",
"sort_by": "Ordenar por",
"sort_popularity": "Popularidad",
"sort_newest": "Más nuevos",
"sort_oldest": "Más viejos",
"sort_title_asc": "Título (A-Z)",
"sort_title_desc": "Título (Z-A)",
"sort_highest_rating": "Mejor valorados",
"sort_lowest_rating": "Peor valorados",
"filters_sidebar_hint": "Usa la barra lateral para refinar tu búsqueda",
"active_filters": "Filtros activos",
"no_genres": "Sin categorías"
},
"game_details": {
"open_download_options": "Abrir opciones de descargas",
+4 -4
View File
@@ -66,7 +66,7 @@
"edit_game_modal_updating": "Mise à jour…",
"edit_game_modal_fill_required": "Veuillez remplir tous les champs requis",
"edit_game_modal_success": "Ressources mises à jour avec succès",
"edit_game_modal_failed": "Échec de la mise à jour des ressources",
"edit_game_modal_failed": "Échec de la mise à jour des elements",
"edit_game_modal_image_filter": "Image",
"edit_game_modal_icon_resolution": "Résolution recommandée : 256x256px",
"edit_game_modal_logo_resolution": "Résolution recommandée : 640x360px",
@@ -81,7 +81,7 @@
"install_decky_plugin": "Installer le plugin Decky",
"update_decky_plugin": "Mettre à jour le plugin Decky",
"decky_plugin_installed_version": "Plugin Decky (v{{version}})",
"install_decky_plugin_title": "Installer le plugin Decky Hydra",
"install_decky_plugin_title": "Installer le plugin Decky",
"install_decky_plugin_message": "Cela téléchargera et installera le plugin Hydra pour Decky Loader. Des permissions élevées peuvent être requises. Continuer ?",
"update_decky_plugin_title": "Mettre à jour le plugin Decky Hydra",
"update_decky_plugin_message": "Une nouvelle version du plugin Decky Hydra est disponible. Souhaitez-vous la mettre à jour maintenant ?",
@@ -166,7 +166,7 @@
"compatibility_requirements": "Compatibilité Linux",
"protondb_minimum": "Niveau ProtonDB minimum",
"steam_deck_minimum": "Support Steam Deck minimum",
"steam_deck_compatible": "Compatible Steam Deck",
"steam_deck_compatible": "Compatible avec Steam Deck",
"compatibility_any": "Tous",
"protondb_silver_plus": "Silver+",
"protondb_gold_plus": "Gold+",
@@ -176,7 +176,7 @@
"release_year": "Année de sortie",
"sort_by": "Trier par",
"most_reviewed": "Les plus commentés",
"best_rated": "Les mieux notés",
"best_rated": "Meilleures notes",
"review_count": "Nombre d'avis",
"release_date": "Date de sortie",
"weekly_top": "Top de la semaine",
+12 -1
View File
@@ -857,7 +857,18 @@
"release_year": "Ano de lançamento",
"filter_by_release_year": "Filtrar por ano de lançamento",
"release_year_gte": "Ano de lançamento a partir de",
"release_year_lte": "Ano de lançamento até"
"release_year_lte": "Ano de lançamento até",
"sort_by": "Ordenar por",
"sort_popularity": "Popularidade",
"sort_newest": "Mais recentes",
"sort_oldest": "Mais antigos",
"sort_title_asc": "Título (A-Z)",
"sort_title_desc": "Título (Z-A)",
"sort_highest_rating": "Mais bem avaliados",
"sort_lowest_rating": "Pior avaliados",
"filters_sidebar_hint": "Use a barra lateral para refinar sua busca",
"active_filters": "Filtros ativos",
"no_genres": "Sem categorias"
},
"modal": {
"close": "Botão de fechar"
+12 -1
View File
@@ -177,7 +177,18 @@
"release_year": "Год выпуска",
"filter_by_release_year": "Фильтровать по году выпуска",
"release_year_gte": "Год выпуска от",
"release_year_lte": "Год выпуска до"
"release_year_lte": "Год выпуска до",
"sort_by": "Сортировать по",
"sort_popularity": "Популярности",
"sort_newest": "Сначала новые",
"sort_oldest": "Сначала старые",
"sort_title_asc": "По названию (A-Z)",
"sort_title_desc": "По названию (Z-A)",
"sort_highest_rating": "По высшему рейтингу",
"sort_lowest_rating": "По низшему рейтингу",
"filters_sidebar_hint": "Используйте боковую панель для уточнения поиска",
"active_filters": "Активные фильтры",
"no_genres": "Без категорий"
},
"game_details": {
"open_download_options": "Открыть источники",
@@ -13,6 +13,8 @@ export interface CatalogueSearchState {
const initialState: CatalogueSearchState = {
filters: {
title: "",
sortBy: "popularity",
sortOrder: "desc",
downloadSourceFingerprints: [],
tags: [],
publishers: [],
@@ -11,16 +11,59 @@
&__filters-container {
width: 100%;
padding: 4px;
align-self: flex-start;
}
&__header {
display: flex;
gap: 8px;
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
&__header-row {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
&--filters {
align-items: flex-start;
}
}
&__header-summary {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
&__filters-hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
&__sort-inline {
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
&__sort-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
white-space: nowrap;
}
&__active-filters-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
text-transform: uppercase;
letter-spacing: 0.04em;
flex-shrink: 0;
align-self: center;
}
&__filters-wrapper {
@@ -48,6 +91,10 @@
align-self: center;
}
&__sort-select {
min-width: 180px;
}
&__content {
display: grid;
grid-template-columns: minmax(0, 1fr) clamp(300px, 22vw, 380px);
@@ -66,12 +113,14 @@
&__pagination-container {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-end;
margin-top: 16px;
}
&__result-count {
font-size: 12px;
font-weight: 500;
color: globals.$body-color;
}
&__filters-sections {
@@ -93,10 +142,20 @@
}
@media (max-width: 768px) {
&__header {
&__header-row {
flex-direction: column;
align-items: stretch;
}
&__sort-inline {
width: 100%;
justify-content: space-between;
}
&__sort-select {
flex: 1;
}
&__clear-all-button {
width: 100%;
}
+139 -46
View File
@@ -18,6 +18,7 @@ import {
import "./catalogue.scss";
import { Button } from "@renderer/components/button/button";
import { SelectField } from "@renderer/components/select-field/select-field";
import { setFilters, setPage } from "@renderer/features";
import { useCatalogue } from "@renderer/hooks/use-catalogue";
import { debounce } from "lodash-es";
@@ -58,7 +59,7 @@ const filterCategoryColors = {
releaseYear: "hsl(38deg 50% 40%)",
};
const PAGE_SIZE = 20;
const PAGE_SIZE = 30;
const clearAllCategoryFilters = {
genres: [],
@@ -71,6 +72,18 @@ const clearAllCategoryFilters = {
releaseYear: undefined,
};
const sortValues = [
"popularity:desc",
"releaseDate:desc",
"releaseDate:asc",
"alphabetical:asc",
"alphabetical:desc",
"hydraScore:desc",
"hydraScore:asc",
] as const;
type CatalogueSortValue = (typeof sortValues)[number];
const protonCompatibilityThresholds: CompatibilityThreshold<
CatalogueSearchPayload["protondbSupportBadges"][number]
>[] = [
@@ -396,6 +409,49 @@ export default function Catalogue() {
const selectedFiltersCount = groupedFilters.length;
const sortOptions = useMemo(
() => [
{
key: "popularity:desc",
value: "popularity:desc",
label: t("sort_popularity"),
},
{
key: "releaseDate:desc",
value: "releaseDate:desc",
label: t("sort_newest"),
},
{
key: "releaseDate:asc",
value: "releaseDate:asc",
label: t("sort_oldest"),
},
{
key: "alphabetical:asc",
value: "alphabetical:asc",
label: t("sort_title_asc"),
},
{
key: "alphabetical:desc",
value: "alphabetical:desc",
label: t("sort_title_desc"),
},
{
key: "hydraScore:desc",
value: "hydraScore:desc",
label: t("sort_highest_rating"),
},
{
key: "hydraScore:asc",
value: "hydraScore:asc",
label: t("sort_lowest_rating"),
},
],
[t]
);
const selectedSortValue = `${filters.sortBy}:${filters.sortOrder}`;
const protonThresholdValue =
protonCompatibilityThresholds.find((threshold) =>
areSameValues(threshold.values, filters.protondbSupportBadges)
@@ -408,50 +464,93 @@ export default function Catalogue() {
return (
<div className="catalogue" ref={cataloguePageRef}>
<div className="catalogue__header">
<div className="catalogue__filters-wrapper">
<ul className="catalogue__filters-list">
{groupedFilters.map((filter) => (
<li key={`${filter.key}-${filter.value}`}>
<FilterItem
filter={filter.label ?? ""}
filterType={filter.filterType}
orbColor={filter.orbColor}
onRemove={() => {
if (filter.value === "range") {
dispatch(setFilters({ releaseYear: undefined }));
return;
}
<div className="catalogue__header-row">
<div className="catalogue__header-summary">
<span className="catalogue__result-count">
{t("result_count", {
resultCount: formatNumber(itemsCount),
})}
</span>
{selectedFiltersCount === 0 && (
<span className="catalogue__filters-hint">
{t("filters_sidebar_hint")}
</span>
)}
</div>
if (filter.value === "threshold") {
dispatch(setFilters({ [filter.key]: [] }));
return;
}
<div className="catalogue__sort-inline">
<span className="catalogue__sort-label">{t("sort_by")}</span>
<SelectField
theme="dark"
className="catalogue__sort-select"
value={
sortValues.includes(selectedSortValue as CatalogueSortValue)
? selectedSortValue
: "popularity:desc"
}
options={sortOptions}
onChange={(event) => {
const [sortBy, sortOrder] = event.target.value.split(":") as [
CatalogueSearchPayload["sortBy"],
CatalogueSearchPayload["sortOrder"],
];
dispatch(
setFilters({
[filter.key]: filters[filter.key].filter(
(item) => item !== filter.value
),
})
);
}}
/>
</li>
))}
</ul>
dispatch(setFilters({ sortBy, sortOrder }));
}}
/>
</div>
</div>
{selectedFiltersCount > 0 && (
<Button
type="button"
theme="outline"
className="catalogue__clear-all-button"
onClick={() => dispatch(setFilters(clearAllCategoryFilters))}
>
{t("clear_filters", {
filterCount: formatNumber(selectedFiltersCount),
})}
</Button>
<div className="catalogue__header-row catalogue__header-row--filters">
<span className="catalogue__active-filters-label">
{t("active_filters")}
</span>
<div className="catalogue__filters-wrapper">
<ul className="catalogue__filters-list">
{groupedFilters.map((filter) => (
<li key={`${filter.key}-${filter.value}`}>
<FilterItem
filter={filter.label ?? ""}
filterType={filter.filterType}
orbColor={filter.orbColor}
onRemove={() => {
if (filter.value === "range") {
dispatch(setFilters({ releaseYear: undefined }));
return;
}
if (filter.value === "threshold") {
dispatch(setFilters({ [filter.key]: [] }));
return;
}
dispatch(
setFilters({
[filter.key]: filters[filter.key].filter(
(item) => item !== filter.value
),
})
);
}}
/>
</li>
))}
</ul>
</div>
<Button
type="button"
theme="outline"
className="catalogue__clear-all-button"
onClick={() => dispatch(setFilters(clearAllCategoryFilters))}
>
{t("clear_filters", {
filterCount: formatNumber(selectedFiltersCount),
})}
</Button>
</div>
)}
</div>
@@ -472,12 +571,6 @@ export default function Catalogue() {
)}
<div className="catalogue__pagination-container">
<span className="catalogue__result-count">
{t("result_count", {
resultCount: formatNumber(itemsCount),
})}
</span>
<Pagination
page={page}
totalPages={Math.ceil(itemsCount / PAGE_SIZE)}
@@ -173,6 +173,11 @@
font-size: 12px;
text-align: left;
margin-bottom: 4px;
&--empty {
color: rgba(255, 255, 255, 0.35);
font-style: italic;
}
}
&__repackers {
@@ -129,7 +129,13 @@ export function GameItem({ game }: GameItemProps) {
<div className="game-item__details">
<span>{game.title}</span>
<span className="game-item__genres">{genres.join(", ")}</span>
{genres && genres.length > 0 ? (
<span className="game-item__genres">{genres.join(", ")}</span>
) : (
<span className="game-item__genres game-item__genres--empty">
{t("no_genres", { ns: "catalogue" })}
</span>
)}
<div className="game-item__repackers">
{game.downloadSources.map((sourceName) => (
@@ -17,6 +17,19 @@
font-size: 12px;
color: globals.$muted-color;
line-height: 1.4;
&--deck {
display: flex;
align-items: center;
gap: 6px;
}
}
&__deck-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
filter: brightness(0) saturate(100%) invert(100%);
}
&__option-list {
@@ -2,6 +2,7 @@ import { ChevronDownIcon } from "@primer/octicons-react";
import { useEffect, useRef, useState } from "react";
import { CheckboxField } from "@renderer/components/checkbox-field/checkbox-field";
import { RadioField } from "@renderer/components/radio-field/radio-field";
import SteamDeckLogo from "@renderer/assets/steam-deck-logo.svg?url";
import "./proton-compatibility-section.scss";
@@ -109,7 +110,13 @@ export function ProtonCompatibilitySection({
</div>
<div className="proton-compatibility-section__control">
<span className="proton-compatibility-section__label">
<span className="proton-compatibility-section__label proton-compatibility-section__label--deck">
<img
src={SteamDeckLogo}
alt=""
aria-hidden="true"
className="proton-compatibility-section__deck-icon"
/>
{deckSliderLabel}
</span>
+7
View File
@@ -429,6 +429,13 @@ export interface ComparedAchievements {
export interface CatalogueSearchPayload {
title: string;
sortBy:
| "popularity"
| "reviewScore"
| "alphabetical"
| "hydraScore"
| "releaseDate";
sortOrder: "asc" | "desc";
downloadSourceFingerprints: string[];
tags: number[];
publishers: string[];
+14 -14
View File
@@ -4114,10 +4114,10 @@ axios-cookiejar-support@^5.0.5:
dependencies:
http-cookie-agent "^6.0.8"
axios@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f"
integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==
axios@^1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b"
integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==
dependencies:
follow-redirects "^1.15.11"
form-data "^4.0.5"
@@ -5567,9 +5567,9 @@ fast-levenshtein@^2.0.6:
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-uri@^3.0.1:
version "3.1.0"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
fast-xml-parser@5.2.5:
version "5.2.5"
@@ -6257,9 +6257,9 @@ internal-slot@^1.1.0:
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
ip-address@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed"
integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
version "10.2.0"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206"
integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==
is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
version "3.0.5"
@@ -9015,10 +9015,10 @@ tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
mkdirp "^1.0.3"
yallist "^4.0.0"
tar@^7.5.13:
version "7.5.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.13.tgz#0d214ed56781a26edc313581c0e2d929ceeb866d"
integrity sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==
tar@^7.5.14:
version "7.5.14"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.14.tgz#4a5021989e5f53a2bdaa2e25592873c1ee3b447c"
integrity sha512-/7sHKgQO3JLP9ESlwTYUUftHUadOURUqq23xs1vjcnp8Vss6k0wCfzulyEtk5g91pjvnuriimGlyG7k6msrzRw==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"