diff --git a/src/main/events/catalogue/search-games.ts b/src/main/events/catalogue/search-games.ts index 09ac2cee..8b22101d 100644 --- a/src/main/events/catalogue/search-games.ts +++ b/src/main/events/catalogue/search-games.ts @@ -2,16 +2,15 @@ import type { CatalogueSearchPayload } from "@types"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -const PAGE_SIZE = 12; - const searchGames = async ( _event: Electron.IpcMainInvokeEvent, payload: CatalogueSearchPayload, - page: number + take: number, + skip: number ) => { return HydraApi.post( "/catalogue/search", - { ...payload, take: page * PAGE_SIZE, skip: (page - 1) * PAGE_SIZE }, + { ...payload, take, skip }, { needsAuth: false } ); }; diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index 48d7a98f..7e924454 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -59,6 +59,9 @@ export const getUserData = () => { bio: "", email: null, profileVisibility: "PUBLIC" as ProfileVisibility, + quirks: { + backupsPerGameLimit: 0, + }, subscription: loggedUser.subscription ? { id: loggedUser.subscription.subscriptionId, diff --git a/src/preload/index.ts b/src/preload/index.ts index 547607fb..e56e6797 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -55,8 +55,8 @@ contextBridge.exposeInMainWorld("electron", { }, /* Catalogue */ - searchGames: (payload: CatalogueSearchPayload, page: number) => - ipcRenderer.invoke("searchGames", payload, page), + searchGames: (payload: CatalogueSearchPayload, take: number, skip: number) => + ipcRenderer.invoke("searchGames", payload, take, skip), getCatalogue: (category: CatalogueCategory) => ipcRenderer.invoke("getCatalogue", category), getGameShopDetails: (objectId: string, shop: GameShop, language: string) => @@ -83,8 +83,6 @@ 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/header/header.tsx b/src/renderer/src/components/header/header.tsx index f53eb6e8..fd4e6510 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react"; @@ -66,6 +66,12 @@ export function Header() { } }; + useEffect(() => { + if (!location.pathname.startsWith("/catalogue")) { + dispatch(setFilters({ title: "" })); + } + }, [location.pathname, dispatch]); + return ( <>
Promise<{ edges: any[]; count: number }>; getCatalogue: (category: CatalogueCategory) => Promise; getGameShopDetails: ( diff --git a/src/renderer/src/hooks/use-catalogue.ts b/src/renderer/src/hooks/use-catalogue.ts new file mode 100644 index 00000000..9c774e83 --- /dev/null +++ b/src/renderer/src/hooks/use-catalogue.ts @@ -0,0 +1,54 @@ +import axios from "axios"; +import { useCallback, useEffect, useState } from "react"; + +export const externalResourcesInstance = axios.create({ + baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL, +}); + +export function useCatalogue() { + const [steamGenres, setSteamGenres] = useState>({}); + const [steamUserTags, setSteamUserTags] = useState< + Record> + >({}); + + const [steamPublishers, setSteamPublishers] = useState([]); + const [steamDevelopers, setSteamDevelopers] = useState([]); + + const getSteamUserTags = useCallback(() => { + externalResourcesInstance.get("/steam-user-tags.json").then((response) => { + setSteamUserTags(response.data); + }); + }, []); + + const getSteamGenres = useCallback(() => { + externalResourcesInstance.get("/steam-genres.json").then((response) => { + setSteamGenres(response.data); + }); + }, []); + + const getSteamPublishers = useCallback(() => { + externalResourcesInstance.get("/steam-publishers.json").then((response) => { + setSteamPublishers(response.data); + }); + }, []); + + const getSteamDevelopers = useCallback(() => { + externalResourcesInstance.get("/steam-developers.json").then((response) => { + setSteamDevelopers(response.data); + }); + }, []); + + useEffect(() => { + getSteamUserTags(); + getSteamGenres(); + getSteamPublishers(); + getSteamDevelopers(); + }, [ + getSteamUserTags, + getSteamGenres, + getSteamPublishers, + getSteamDevelopers, + ]); + + return { steamGenres, steamUserTags, steamPublishers, steamDevelopers }; +} diff --git a/src/renderer/src/hooks/use-repacks.ts b/src/renderer/src/hooks/use-repacks.ts index 1c160be6..dbc918b9 100644 --- a/src/renderer/src/hooks/use-repacks.ts +++ b/src/renderer/src/hooks/use-repacks.ts @@ -19,7 +19,13 @@ export function useRepacks() { const updateRepacks = useCallback(() => { repacksTable.toArray().then((repacks) => { dispatch( - setRepacks(repacks.filter((repack) => Array.isArray(repack.objectIds))) + setRepacks( + JSON.parse( + JSON.stringify( + repacks.filter((repack) => Array.isArray(repack.objectIds)) + ) + ) + ) ); }); }, [dispatch]); diff --git a/src/renderer/src/pages/catalogue/catalogue.scss b/src/renderer/src/pages/catalogue/catalogue.scss index e1ba3113..18cc18be 100644 --- a/src/renderer/src/pages/catalogue/catalogue.scss +++ b/src/renderer/src/pages/catalogue/catalogue.scss @@ -1,17 +1,5 @@ @use "../../scss/globals.scss"; -@keyframes gradientBorder { - 0% { - border-image-source: linear-gradient(0deg, #16b195 50%, #3e62c0 100%); - } - 50% { - border-image-source: linear-gradient(90deg, #3e62c0 50%, #16b195 100%); - } - 100% { - border-image-source: linear-gradient(180deg, #16b195 50%, #3e62c0 100%); - } -} - .catalogue { display: flex; flex-direction: column; @@ -19,25 +7,6 @@ width: 100%; padding: 16px; - &__game-item { - background-color: globals.$dark-background-color; - width: 100%; - color: #fff; - display: flex; - align-items: center; - overflow: hidden; - position: relative; - border-radius: 4px; - border: 1px solid globals.$border-color; - cursor: pointer; - gap: 12px; - transition: all ease 0.2s; - - &:hover { - background-color: rgba(255, 255, 255, 0.05); - } - } - &__filters-container { width: 270px; min-width: 270px; diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx index 82457f7d..b6d735e1 100644 --- a/src/renderer/src/pages/catalogue/catalogue.tsx +++ b/src/renderer/src/pages/catalogue/catalogue.tsx @@ -1,23 +1,25 @@ -import { Badge, Button } from "@renderer/components"; - import type { DownloadSource } from "@types"; -import { useAppDispatch, useAppSelector, useRepacks } from "@renderer/hooks"; +import { + useAppDispatch, + useAppSelector, + useFormat, + useRepacks, +} from "@renderer/hooks"; import { useEffect, useMemo, useRef, useState } from "react"; -import { XIcon } from "@primer/octicons-react"; import "./catalogue.scss"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { downloadSourcesTable } from "@renderer/dexie"; -import { steamUrlBuilder } from "@shared"; -import { buildGameDetailsPath } from "@renderer/helpers"; -import { useNavigate } from "react-router-dom"; import { FilterSection } from "./filter-section"; import { setFilters } from "@renderer/features"; import { useTranslation } from "react-i18next"; -import axios from "axios"; import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import { Pagination } from "./pagination"; +import { useCatalogue } from "@renderer/hooks/use-catalogue"; +import { GameItem } from "./game-item"; +import { FilterItem } from "./filter-item"; const filterCategoryColors = { genres: "hsl(262deg 50% 47%)", @@ -27,21 +29,23 @@ const filterCategoryColors = { publishers: "hsl(200deg 50% 30%)", }; +const PAGE_SIZE = 20; + export default function Catalogue() { const abortControllerRef = useRef(null); - const [steamUserTags, setSteamUserTags] = useState({}); - - const navigate = useNavigate(); + const { steamGenres, steamUserTags, steamDevelopers, steamPublishers } = + useCatalogue(); const [downloadSources, setDownloadSources] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [publishers, setPublishers] = useState([]); - const [developers, setDevelopers] = useState([]); const [results, setResults] = useState([]); + const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); + const [itemsCount, setItemsCount] = useState(0); + + const { formatNumber } = useFormat(); const { filters } = useAppSelector((state) => state.catalogueSearch); @@ -60,61 +64,159 @@ export default function Catalogue() { abortControllerRef.current = abortController; window.electron - .searchGames(filters, page) + .searchGames(filters, PAGE_SIZE, (page - 1) * PAGE_SIZE) .then((response) => { if (abortController.signal.aborted) { return; } setResults(response.edges); - setTotalPages(Math.ceil(response.count / 12)); - }) - .finally(() => { + setItemsCount(response.count); setIsLoading(false); }); }, [filters, page, dispatch]); - useEffect(() => { - window.electron.getDevelopers().then((developers) => { - setDevelopers(developers); - }); - - window.electron.getPublishers().then((publishers) => { - setPublishers(publishers); - }); - }, []); - - const gamesWithRepacks = useMemo(() => { - return results.map((game) => { - const repacks = getRepacksForObjectId(game.objectId); - const uniqueRepackers = Array.from( - new Set(repacks.map((repack) => repack.repacker)) - ); - return { ...game, repacks: uniqueRepackers }; - }); - }, [results, getRepacksForObjectId]); - useEffect(() => { downloadSourcesTable.toArray().then((sources) => { setDownloadSources(sources.filter((source) => !!source.fingerprint)); }); }, [getRepacksForObjectId]); - useEffect(() => { - axios - .get( - `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/steam-user-tags.json` - ) - .then((response) => { - const language = i18n.language.split("-")[0]; + const language = i18n.language.split("-")[0]; - if (response.data[language]) { - setSteamUserTags(response.data[language]); - } else { - setSteamUserTags(response.data["en"]); - } - }); - }, [i18n.language]); + const steamGenresMapping = useMemo>(() => { + if (!steamGenres[language]) return {}; + + return steamGenres[language].reduce((prev, genre, index) => { + prev[genre] = steamGenres["en"][index]; + return prev; + }, {}); + }, [steamGenres, language]); + + const steamGenresFilterItems = useMemo(() => { + return Object.entries(steamGenresMapping) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + .map(([key, value]) => ({ + label: key, + value: value, + checked: filters.genres.includes(value), + })); + }, [steamGenresMapping, filters.genres]); + + useEffect(() => { + setPage(1); + }, [filters]); + + const steamUserTagsFilterItems = useMemo(() => { + if (!steamUserTags[language]) return []; + + return Object.entries(steamUserTags[language]) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + .map(([key, value]) => ({ + label: key, + value: value.toString(), + checked: filters.tags.includes(value), + })); + }, [steamUserTags, filters.tags, language]); + + const groupedFilters = useMemo(() => { + return [ + ...filters.genres.map((genre) => ({ + label: Object.keys(steamGenresMapping).find( + (key) => steamGenresMapping[key] === genre + ) as string, + orbColor: filterCategoryColors.genres, + key: "genres", + value: genre, + })), + + ...filters.tags.map((tag) => ({ + label: Object.keys(steamUserTags[language]).find( + (key) => steamUserTags[language][key] === tag + ) as string, + orbColor: filterCategoryColors.tags, + key: "tags", + value: tag, + })), + + ...filters.downloadSourceFingerprints.map((fingerprint) => ({ + label: downloadSources.find( + (source) => source.fingerprint === fingerprint + )?.name as string, + orbColor: filterCategoryColors.downloadSourceFingerprints, + key: "downloadSourceFingerprints", + value: fingerprint, + })), + + ...filters.developers.map((developer) => ({ + label: developer, + orbColor: filterCategoryColors.developers, + key: "developers", + value: developer, + })), + + ...filters.publishers.map((publisher) => ({ + label: publisher, + orbColor: filterCategoryColors.publishers, + key: "publishers", + value: publisher, + })), + ]; + }, [filters, steamUserTags, steamGenresMapping, language, downloadSources]); + + const filterSections = useMemo(() => { + return [ + { + title: t("genres"), + items: steamGenresFilterItems, + key: "genres", + }, + { + title: t("tags"), + items: steamUserTagsFilterItems, + key: "tags", + }, + { + title: t("download_sources"), + items: downloadSources.map((source) => ({ + label: source.name, + value: source.fingerprint, + checked: filters.downloadSourceFingerprints.includes( + source.fingerprint + ), + })), + key: "downloadSourceFingerprints", + }, + { + title: t("developers"), + items: steamDevelopers.map((developer) => ({ + label: developer, + value: developer, + checked: filters.developers.includes(developer), + })), + key: "developers", + }, + { + title: t("publishers"), + items: steamPublishers.map((publisher) => ({ + label: publisher, + value: publisher, + checked: filters.publishers.includes(publisher), + })), + key: "publishers", + }, + ]; + }, [ + downloadSources, + filters.developers, + filters.downloadSourceFingerprints, + filters.publishers, + steamDevelopers, + steamGenresFilterItems, + steamPublishers, + steamUserTagsFilterItems, + t, + ]); return (
@@ -127,65 +229,35 @@ export default function Catalogue() { }} >
- {filters.genres.map((genre) => ( - -
-
- - {genre} -
- - ))} - -
  • -
    - Action - -
  • + {groupedFilters.map((filter) => ( +
  • + { + dispatch( + setFilters({ + [filter.key]: filters[filter.key].filter( + (item) => item !== filter.value + ), + }) + ); + }} + /> +
  • + ))} +
    - - {/* */}
    - {Array.from({ length: 24 }).map((_, i) => ( + {Array.from({ length: PAGE_SIZE }).map((_, i) => ( ) : ( - gamesWithRepacks.map((game, i) => ( - - )) + results.map((game) => ) )} - {totalPages > 1 && ( -
    - {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
    - )} +
    + {formatNumber(itemsCount)} resultados + + +
    - dispatch(setFilters({ genres: [] }))} - color={filterCategoryColors.genres} - onSelect={(value) => { - if (filters.genres.includes(value)) { - dispatch( - setFilters({ - genres: filters.genres.filter((genre) => genre !== value), - }) - ); - } else { - dispatch(setFilters({ genres: [...filters.genres, value] })); - } - }} - items={[ - "Action", - "Strategy", - "RPG", - "Casual", - "Racing", - "Sports", - "Indie", - "Adventure", - "Simulation", - "Massively Multiplayer", - "Free to Play", - "Accounting", - "Animation & Modeling", - "Audio Production", - "Design & Illustration", - "Education", - "Photo Editing", - "Software Training", - "Utilities", - "Video Production", - "Web Publishing", - "Game Development", - "Early Access", - "Sexual Content", - "Nudity", - "Violent", - "Gore", - "Documentary", - "Tutorial", - ] - .sort() - .map((genre) => ({ - label: genre, - value: genre, - checked: filters.genres.includes(genre), - }))} - /> - - dispatch(setFilters({ tags: [] }))} - onSelect={(value) => { - if (filters.tags.includes(Number(value))) { - dispatch( - setFilters({ - tags: filters.tags.filter((tag) => tag !== Number(value)), - }) - ); - } else { - dispatch( - setFilters({ tags: [...filters.tags, Number(value)] }) - ); - } - }} - items={ - Object.entries(steamUserTags) - .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) - .map(([key, value]) => ({ - label: key, - value: value, - checked: filters.tags.includes(value as number), - })) as any - } - /> - - - dispatch(setFilters({ downloadSourceFingerprints: [] })) - } - onSelect={(value) => { - if (filters.downloadSourceFingerprints.includes(value)) { - dispatch( - setFilters({ - downloadSourceFingerprints: - filters.downloadSourceFingerprints.filter( - (fingerprint) => fingerprint !== value - ), - }) - ); - } else { - dispatch( - setFilters({ - downloadSourceFingerprints: [ - ...filters.downloadSourceFingerprints, - value, - ], - }) - ); - } - }} - items={downloadSources.map((downloadSource) => ({ - label: downloadSource.name, - value: downloadSource.fingerprint, - checked: filters.downloadSourceFingerprints.includes( - downloadSource.fingerprint - ), - }))} - /> - - dispatch(setFilters({ developers: [] }))} - onSelect={(value) => { - if (filters.developers.includes(value)) { - dispatch( - setFilters({ - developers: filters.developers.filter( - (developer) => developer !== value - ), - }) - ); - } else { - dispatch( - setFilters({ developers: [...filters.developers, value] }) - ); - } - }} - items={developers.map((developer) => ({ - label: developer, - value: developer, - checked: filters.developers.includes(developer), - }))} - /> - - dispatch(setFilters({ publishers: [] }))} - onSelect={(value) => { - if (filters.publishers.includes(value)) { - dispatch( - setFilters({ - publishers: filters.publishers.filter( - (publisher) => publisher !== value - ), - }) - ); - } else { - dispatch( - setFilters({ publishers: [...filters.publishers, value] }) - ); - } - }} - items={publishers.map((publisher) => ({ - label: publisher, - value: publisher, - checked: filters.publishers.includes(publisher), - }))} - /> + {filterSections.map((section) => ( + dispatch(setFilters({ [section.key]: [] }))} + color={filterCategoryColors[section.key]} + onSelect={(value) => { + if (filters[section.key].includes(value)) { + dispatch( + setFilters({ + [section.key]: filters[ + section.key as + | "genres" + | "tags" + | "downloadSourceFingerprints" + | "developers" + | "publishers" + ].filter((item) => item !== value), + }) + ); + } else { + dispatch( + setFilters({ + [section.key]: [...filters[section.key], value], + }) + ); + } + }} + items={section.items} + /> + ))}
    diff --git a/src/renderer/src/pages/catalogue/filter-item.tsx b/src/renderer/src/pages/catalogue/filter-item.tsx new file mode 100644 index 00000000..2413bee9 --- /dev/null +++ b/src/renderer/src/pages/catalogue/filter-item.tsx @@ -0,0 +1,50 @@ +import { vars } from "@renderer/theme.css"; +import { XIcon } from "@primer/octicons-react"; + +interface FilterItemProps { + filter: string; + orbColor: string; + onRemove: () => void; +} + +export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) { + return ( +
    +
    + {filter} + +
    + ); +} diff --git a/src/renderer/src/pages/catalogue/game-item.scss b/src/renderer/src/pages/catalogue/game-item.scss new file mode 100644 index 00000000..83d182f4 --- /dev/null +++ b/src/renderer/src/pages/catalogue/game-item.scss @@ -0,0 +1,48 @@ +@use "../../scss/globals.scss"; + +.game-item { + background-color: globals.$dark-background-color; + width: 100%; + color: #fff; + display: flex; + align-items: center; + overflow: hidden; + position: relative; + border-radius: 4px; + border: 1px solid globals.$border-color; + cursor: pointer; + gap: calc(globals.$spacing-unit * 2); + transition: all ease 0.2s; + + &:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + &__cover { + width: 200px; + height: 100%; + object-fit: cover; + border-right: 1px solid globals.$border-color; + } + + &__details { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + padding: calc(globals.$spacing-unit * 2) 0; + } + + &__genres { + color: globals.$body-color; + font-size: 12px; + text-align: left; + margin-bottom: 4px; + } + + &__repackers { + display: flex; + gap: globals.$spacing-unit; + flex-wrap: wrap; + } +} diff --git a/src/renderer/src/pages/catalogue/game-item.tsx b/src/renderer/src/pages/catalogue/game-item.tsx new file mode 100644 index 00000000..06f30998 --- /dev/null +++ b/src/renderer/src/pages/catalogue/game-item.tsx @@ -0,0 +1,50 @@ +import { Badge } from "@renderer/components"; +import { buildGameDetailsPath } from "@renderer/helpers"; +import { useRepacks } from "@renderer/hooks"; +import { steamUrlBuilder } from "@shared"; +import { useMemo } from "react"; +import { useNavigate } from "react-router-dom"; + +import "./game-item.scss"; + +export interface GameItemProps { + game: any; +} + +export function GameItem({ game }: GameItemProps) { + const navigate = useNavigate(); + + const { getRepacksForObjectId } = useRepacks(); + + const repacks = getRepacksForObjectId(game.objectId); + + const uniqueRepackers = useMemo(() => { + return Array.from(new Set(repacks.map((repack) => repack.repacker))); + }, [repacks]); + + return ( + + ); +} diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx new file mode 100644 index 00000000..05ac8c4d --- /dev/null +++ b/src/renderer/src/pages/catalogue/pagination.tsx @@ -0,0 +1,83 @@ +import { Button } from "@renderer/components/button/button"; +import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react"; + +interface PaginationProps { + page: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +export function Pagination({ + page, + totalPages, + onPageChange, +}: PaginationProps) { + if (totalPages <= 1) return null; + + // Number of visible pages + const visiblePages = 3; + + // Calculate the start and end of the visible range + let startPage = Math.max(1, page - 1); // Shift range slightly back + let endPage = startPage + visiblePages - 1; + + // Adjust the range if we're near the start or end + if (endPage > totalPages) { + endPage = totalPages; + startPage = Math.max(1, endPage - visiblePages + 1); + } + + return ( +
    + {/* Previous Button */} + + +
    + ... +
    + + {/* Page Buttons */} + {Array.from( + { length: endPage - startPage + 1 }, + (_, i) => startPage + i + ).map((pageNumber) => ( + + ))} + + {/* Next Button */} + +
    + ); +} diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index be5cfd4d..b4665f15 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -14,7 +14,7 @@ import { TrashIcon, UploadIcon, } from "@primer/octicons-react"; -import { useToast } from "@renderer/hooks"; +import { useAppSelector, useToast } from "@renderer/hooks"; import { useTranslation } from "react-i18next"; import { AxiosProgressEvent } from "axios"; import { formatDownloadProgress } from "@renderer/helpers"; @@ -145,6 +145,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { const disableActions = uploadingBackup || restoringBackup || deletingArtifact; + const userDetails = useAppSelector((state) => state.userDetails.userDetails); + const backupsPerGameLimit = userDetails?.quirks.backupsPerGameLimit ?? 0; + return ( = 2 + artifacts.length >= backupsPerGameLimit } > @@ -199,7 +202,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { }} >

    {t("backups")}

    - {artifacts.length} / 2 + + {artifacts.length} / {backupsPerGameLimit} +
    diff --git a/src/renderer/src/pages/settings/settings-download-sources.css.ts b/src/renderer/src/pages/settings/settings-download-sources.css.ts index 0e88631d..caa93ce8 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.css.ts +++ b/src/renderer/src/pages/settings/settings-download-sources.css.ts @@ -42,3 +42,17 @@ export const downloadSourcesHeader = style({ justifyContent: "space-between", alignItems: "center", }); + +export const navigateToCatalogueButton = style({ + display: "flex", + alignItems: "center", + gap: `${SPACING_UNIT}px`, + color: vars.color.muted, + textDecoration: "underline", + cursor: "pointer", + + ":disabled": { + cursor: "default", + textDecoration: "none", + }, +}); diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index aabbab27..96fb6f9f 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -9,7 +9,6 @@ import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react"; import { AddDownloadSourceModal } from "./add-download-source-modal"; import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks"; import { DownloadSourceStatus } from "@shared"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { settingsContext } from "@renderer/context"; import { downloadSourcesTable } from "@renderer/dexie"; import { downloadSourcesWorker } from "@renderer/workers"; @@ -163,14 +162,8 @@ export function SettingsDownloadSources() {