diff --git a/package.json b/package.json
index 55a7f3bb4..ee93d418c 100644
--- a/package.json
+++ b/package.json
@@ -62,11 +62,13 @@
"lodash-es": "^4.17.21",
"parse-torrent": "^11.0.17",
"piscina": "^4.7.0",
+ "rc-virtual-list": "^3.16.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^14.1.0",
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
+ "react-virtualized": "^9.22.5",
"sound-play": "^1.1.0",
"sudo-prompt": "^9.2.1",
"tar": "^7.4.3",
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 35863eac8..723127e82 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -8,6 +8,8 @@ import "./catalogue/get-random-game";
import "./catalogue/search-games";
import "./catalogue/get-game-stats";
import "./catalogue/get-trending-games";
+import "./catalogue/get-publishers";
+import "./catalogue/get-developers";
import "./hardware/get-disk-free-space";
import "./library/add-game-to-library";
import "./library/create-game-shortcut";
diff --git a/src/preload/index.ts b/src/preload/index.ts
index cab56fe23..86414e1a6 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -65,6 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
listener
);
},
+ getPublishers: () => ipcRenderer.invoke("getPublishers"),
+ getDevelopers: () => ipcRenderer.invoke("getDevelopers"),
/* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
diff --git a/src/renderer/src/components/badge/badge.tsx b/src/renderer/src/components/badge/badge.tsx
index 752a33ba1..c4819ae38 100644
--- a/src/renderer/src/components/badge/badge.tsx
+++ b/src/renderer/src/components/badge/badge.tsx
@@ -7,9 +7,5 @@ export interface BadgeProps {
}
export function Badge({ children }: BadgeProps) {
- return (
-
- {children}
-
- );
+ return {children}
;
}
diff --git a/src/renderer/src/components/checkbox-field/checkbox-field.css.ts b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts
index 6546d9428..b4d3afc8d 100644
--- a/src/renderer/src/components/checkbox-field/checkbox-field.css.ts
+++ b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts
@@ -25,6 +25,7 @@ export const checkbox = recipe({
border: `solid 1px ${vars.color.border}`,
minWidth: "20px",
minHeight: "20px",
+ color: vars.color.darkBackground,
":hover": {
borderColor: "rgba(255, 255, 255, 0.5)",
},
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index c809b4b3e..c8033d7e8 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -68,6 +68,8 @@ declare global {
shop: GameShop,
cb: (achievements: GameAchievement[]) => void
) => () => Electron.IpcRenderer;
+ getPublishers: () => Promise;
+ getDevelopers: () => Promise;
/* Library */
addGameToLibrary: (
diff --git a/src/renderer/src/features/catalogue-search.ts b/src/renderer/src/features/catalogue-search.ts
index 90fd5051a..a6318a262 100644
--- a/src/renderer/src/features/catalogue-search.ts
+++ b/src/renderer/src/features/catalogue-search.ts
@@ -34,4 +34,4 @@ export const catalogueSearchSlice = createSlice({
},
});
-export const { setSearch } = catalogueSearchSlice.actions;
+export const { setSearch, clearSearch } = catalogueSearchSlice.actions;
diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx
index a99ac0500..9bbcd3586 100644
--- a/src/renderer/src/pages/catalogue/catalogue.tsx
+++ b/src/renderer/src/pages/catalogue/catalogue.tsx
@@ -20,6 +20,14 @@ import { setSearch } from "@renderer/features";
import { useTranslation } from "react-i18next";
import { steamUserTags } from "./steam-user-tags";
+const filterCategoryColors = {
+ genres: "hsl(262deg 50% 47%)",
+ tags: "hsl(95deg 50% 20%)",
+ downloadSourceFingerprints: "hsl(27deg 50% 40%)",
+ developers: "hsl(340deg 50% 46%)",
+ publishers: "hsl(200deg 50% 30%)",
+};
+
export default function Catalogue() {
const inputRef = useRef(null);
@@ -34,6 +42,8 @@ export default function Catalogue() {
const [downloadSources, setDownloadSources] = useState([]);
const [games, setGames] = useState([]);
+ const [publishers, setPublishers] = useState([]);
+ const [developers, setDevelopers] = useState([]);
const filters = useAppSelector((state) => state.catalogueSearch.value);
@@ -59,6 +69,16 @@ export default function Catalogue() {
});
}, [filters]);
+ useEffect(() => {
+ window.electron.getDevelopers().then((developers) => {
+ setDevelopers(developers);
+ });
+
+ window.electron.getPublishers().then((publishers) => {
+ setPublishers(publishers);
+ });
+ }, []);
+
const gamesWithRepacks = useMemo(() => {
return games.map((game) => {
const repacks = getRepacksForObjectId(game.objectId);
@@ -148,13 +168,50 @@ export default function Catalogue() {
}}
>
+ {filters.genres.map((genre) => (
+
+
+
+ ))}
+
+ {filters.tags.map((tag) => (
+
+
+ {tag}
+
+
+ ))}
+
{filters.downloadSourceFingerprints.map((fingerprint) => (
- {
- downloadSources.find(
- (source) => source.fingerprint === fingerprint
- )?.name
- }
+
+
+
+ {
+ downloadSources.find(
+ (source) => source.fingerprint === fingerprint
+ )?.name
+ }
+
))}
@@ -248,6 +305,7 @@ export default function Catalogue() {
{
if (filters.genres.includes(value)) {
dispatch(
@@ -300,6 +358,7 @@ export default function Catalogue() {
{
if (filters.tags.includes(value)) {
dispatch(
@@ -322,6 +381,7 @@ export default function Catalogue() {
{
if (filters.downloadSourceFingerprints.includes(value)) {
dispatch(
@@ -351,6 +411,56 @@ export default function Catalogue() {
),
}))}
/>
+
+ {
+ if (filters.developers.includes(value)) {
+ dispatch(
+ setSearch({
+ developers: filters.developers.filter(
+ (developer) => developer !== value
+ ),
+ })
+ );
+ } else {
+ dispatch(
+ setSearch({ developers: [...filters.developers, value] })
+ );
+ }
+ }}
+ items={developers.map((developer) => ({
+ label: developer,
+ value: developer,
+ checked: filters.developers.includes(developer),
+ }))}
+ />
+
+ {
+ if (filters.publishers.includes(value)) {
+ dispatch(
+ setSearch({
+ publishers: filters.publishers.filter(
+ (publisher) => publisher !== value
+ ),
+ })
+ );
+ } else {
+ dispatch(
+ setSearch({ publishers: [...filters.publishers, value] })
+ );
+ }
+ }}
+ items={publishers.map((publisher) => ({
+ label: publisher,
+ value: publisher,
+ checked: filters.publishers.includes(publisher),
+ }))}
+ />
diff --git a/src/renderer/src/pages/catalogue/filter-section.tsx b/src/renderer/src/pages/catalogue/filter-section.tsx
index 940394a18..ea1b7224b 100644
--- a/src/renderer/src/pages/catalogue/filter-section.tsx
+++ b/src/renderer/src/pages/catalogue/filter-section.tsx
@@ -2,6 +2,8 @@ import { CheckboxField, TextField } from "@renderer/components";
import { useFormat } from "@renderer/hooks";
import { useCallback, useMemo, useState } from "react";
+import List from "rc-virtual-list";
+
export interface FilterSectionProps {
title: string;
items: {
@@ -10,11 +12,13 @@ export interface FilterSectionProps {
checked: boolean;
}[];
onSelect: (value: T) => void;
+ color: string;
}
export function FilterSection({
title,
items,
+ color,
onSelect,
}: FilterSectionProps) {
const [search, setSearch] = useState("");
@@ -37,15 +41,25 @@ export function FilterSection({
return (
-
- {title}
-
+
{formatNumber(items.length)} disponÃveis
@@ -59,25 +73,31 @@ export function FilterSection({
theme="dark"
/>
-
- {filteredItems.map((item) => (
-
+ {(item) => (
+
onSelect(item.value)}
/>
- ))}
-
+ )}
+
);
}
diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx
index 94846fc57..f5c01a21c 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.tsx
+++ b/src/renderer/src/pages/settings/settings-download-sources.tsx
@@ -7,12 +7,14 @@ import * as styles from "./settings-download-sources.css";
import type { DownloadSource } from "@types";
import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal";
-import { useRepacks, useToast } from "@renderer/hooks";
+import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks";
import { DownloadSourceStatus } from "@shared";
-import { SPACING_UNIT } from "@renderer/theme.css";
+import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { settingsContext } from "@renderer/context";
import { downloadSourcesTable } from "@renderer/dexie";
import { downloadSourcesWorker } from "@renderer/workers";
+import { clearSearch, setSearch } from "@renderer/features";
+import { useNavigate } from "react-router-dom";
export function SettingsDownloadSources() {
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
@@ -28,6 +30,10 @@ export function SettingsDownloadSources() {
const { t } = useTranslation("settings");
const { showSuccessToast } = useToast();
+ const dispatch = useAppDispatch();
+
+ const navigate = useNavigate();
+
const { updateRepacks } = useRepacks();
const getDownloadSources = async () => {
@@ -96,6 +102,13 @@ export function SettingsDownloadSources() {
setShowAddDownloadSourceModal(false);
};
+ const navigateToCatalogue = (fingerprint: string) => {
+ dispatch(clearSearch());
+ dispatch(setSearch({ downloadSourceFingerprints: [fingerprint] }));
+
+ navigate("/catalogue");
+ };
+
return (
<>
{statusTitle[downloadSource.status]}
- navigateToCatalogue(downloadSource.fingerprint)}
>
{t("download_count", {
@@ -161,7 +179,7 @@ export function SettingsDownloadSources() {
downloadSource.downloadCount.toLocaleString(),
})}
-
+