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) => ( + +
+
+ + {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} -

+
+
+

+ {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(), })} -
+