mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
feat: adding remove function to filter tags
This commit is contained in:
parent
4476b1b216
commit
b5a9beb481
@ -2,16 +2,15 @@ import type { CatalogueSearchPayload } from "@types";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
const PAGE_SIZE = 12;
|
|
||||||
|
|
||||||
const searchGames = async (
|
const searchGames = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
payload: CatalogueSearchPayload,
|
payload: CatalogueSearchPayload,
|
||||||
page: number
|
take: number,
|
||||||
|
skip: number
|
||||||
) => {
|
) => {
|
||||||
return HydraApi.post(
|
return HydraApi.post(
|
||||||
"/catalogue/search",
|
"/catalogue/search",
|
||||||
{ ...payload, take: page * PAGE_SIZE, skip: (page - 1) * PAGE_SIZE },
|
{ ...payload, take, skip },
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -37,8 +37,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (payload: CatalogueSearchPayload, page: number) =>
|
searchGames: (payload: CatalogueSearchPayload, take: number, skip: number) =>
|
||||||
ipcRenderer.invoke("searchGames", payload, page),
|
ipcRenderer.invoke("searchGames", payload, take, skip),
|
||||||
getCatalogue: (category: CatalogueCategory) =>
|
getCatalogue: (category: CatalogueCategory) =>
|
||||||
ipcRenderer.invoke("getCatalogue", category),
|
ipcRenderer.invoke("getCatalogue", category),
|
||||||
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
||||||
@ -65,8 +65,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
listener
|
listener
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getPublishers: () => ipcRenderer.invoke("getPublishers"),
|
|
||||||
getDevelopers: () => ipcRenderer.invoke("getDevelopers"),
|
|
||||||
|
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
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 { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
|
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
@ -51,7 +51,8 @@ declare global {
|
|||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (
|
searchGames: (
|
||||||
payload: CatalogueSearchPayload,
|
payload: CatalogueSearchPayload,
|
||||||
page: number
|
take: number,
|
||||||
|
skip: number
|
||||||
) => Promise<{ edges: any[]; count: number }>;
|
) => Promise<{ edges: any[]; count: number }>;
|
||||||
getCatalogue: (category: CatalogueCategory) => Promise<any[]>;
|
getCatalogue: (category: CatalogueCategory) => Promise<any[]>;
|
||||||
getGameShopDetails: (
|
getGameShopDetails: (
|
||||||
|
54
src/renderer/src/hooks/use-catalogue.ts
Normal file
54
src/renderer/src/hooks/use-catalogue.ts
Normal file
@ -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<Record<string, string[]>>({});
|
||||||
|
const [steamUserTags, setSteamUserTags] = useState<
|
||||||
|
Record<string, Record<string, number>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const [steamPublishers, setSteamPublishers] = useState<string[]>([]);
|
||||||
|
const [steamDevelopers, setSteamDevelopers] = useState<string[]>([]);
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
@ -19,7 +19,13 @@ export function useRepacks() {
|
|||||||
const updateRepacks = useCallback(() => {
|
const updateRepacks = useCallback(() => {
|
||||||
repacksTable.toArray().then((repacks) => {
|
repacksTable.toArray().then((repacks) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setRepacks(repacks.filter((repack) => Array.isArray(repack.objectIds)))
|
setRepacks(
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
repacks.filter((repack) => Array.isArray(repack.objectIds))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
@ -1,17 +1,5 @@
|
|||||||
@use "../../scss/globals.scss";
|
@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 {
|
.catalogue {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -19,25 +7,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
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 {
|
&__filters-container {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
min-width: 270px;
|
min-width: 270px;
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
import { Badge, Button } from "@renderer/components";
|
|
||||||
|
|
||||||
import type { DownloadSource } from "@types";
|
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 { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { XIcon } from "@primer/octicons-react";
|
|
||||||
|
|
||||||
import "./catalogue.scss";
|
import "./catalogue.scss";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { downloadSourcesTable } from "@renderer/dexie";
|
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 { FilterSection } from "./filter-section";
|
||||||
import { setFilters } from "@renderer/features";
|
import { setFilters } from "@renderer/features";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import axios from "axios";
|
|
||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
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 = {
|
const filterCategoryColors = {
|
||||||
genres: "hsl(262deg 50% 47%)",
|
genres: "hsl(262deg 50% 47%)",
|
||||||
@ -27,21 +29,23 @@ const filterCategoryColors = {
|
|||||||
publishers: "hsl(200deg 50% 30%)",
|
publishers: "hsl(200deg 50% 30%)",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 [steamUserTags, setSteamUserTags] = useState<any>({});
|
const { steamGenres, steamUserTags, steamDevelopers, steamPublishers } =
|
||||||
|
useCatalogue();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [publishers, setPublishers] = useState<string[]>([]);
|
|
||||||
const [developers, setDevelopers] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [results, setResults] = useState<any[]>([]);
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [itemsCount, setItemsCount] = useState(0);
|
||||||
|
|
||||||
|
const { formatNumber } = useFormat();
|
||||||
|
|
||||||
const { filters } = useAppSelector((state) => state.catalogueSearch);
|
const { filters } = useAppSelector((state) => state.catalogueSearch);
|
||||||
|
|
||||||
@ -60,61 +64,155 @@ export default function Catalogue() {
|
|||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
window.electron
|
window.electron
|
||||||
.searchGames(filters, page)
|
.searchGames(filters, PAGE_SIZE, (page - 1) * PAGE_SIZE)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setResults(response.edges);
|
setResults(response.edges);
|
||||||
setTotalPages(Math.ceil(response.count / 12));
|
setItemsCount(response.count);
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, [filters, page, dispatch]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
downloadSourcesTable.toArray().then((sources) => {
|
downloadSourcesTable.toArray().then((sources) => {
|
||||||
setDownloadSources(sources.filter((source) => !!source.fingerprint));
|
setDownloadSources(sources.filter((source) => !!source.fingerprint));
|
||||||
});
|
});
|
||||||
}, [getRepacksForObjectId]);
|
}, [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]) {
|
const steamGenresMapping = useMemo<Record<string, string>>(() => {
|
||||||
setSteamUserTags(response.data[language]);
|
if (!steamGenres[language]) return {};
|
||||||
} else {
|
|
||||||
setSteamUserTags(response.data["en"]);
|
return steamGenres[language].reduce((prev, genre, index) => {
|
||||||
}
|
prev[genre] = steamGenres["en"][index];
|
||||||
});
|
return prev;
|
||||||
}, [i18n.language]);
|
}, {});
|
||||||
|
}, [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]);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="catalogue">
|
<div className="catalogue">
|
||||||
@ -127,65 +225,35 @@ export default function Catalogue() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||||
{filters.genres.map((genre) => (
|
<ul
|
||||||
<Badge key={genre}>
|
|
||||||
<div style={{ display: "flex", gap: 4, alignItems: "center" }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
backgroundColor: filterCategoryColors.genres,
|
|
||||||
borderRadius: "50%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{genre}
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<li
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
gap: 8,
|
||||||
color: vars.color.body,
|
flexWrap: "wrap",
|
||||||
backgroundColor: vars.color.darkBackground,
|
listStyle: "none",
|
||||||
padding: "6px 12px",
|
margin: 0,
|
||||||
borderRadius: 4,
|
padding: 0,
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{groupedFilters.map((filter) => (
|
||||||
style={{
|
<li key={filter.label}>
|
||||||
width: 10,
|
<FilterItem
|
||||||
height: 10,
|
filter={filter.label}
|
||||||
backgroundColor: filterCategoryColors.genres,
|
orbColor={filter.orbColor}
|
||||||
borderRadius: "50%",
|
onRemove={() => {
|
||||||
marginRight: 8,
|
dispatch(
|
||||||
|
setFilters({
|
||||||
|
[filter.key]: filters[filter.key].filter(
|
||||||
|
(item) => item !== filter.value
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Action
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
color: vars.color.body,
|
|
||||||
marginLeft: 4,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XIcon size={13} />
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <Button theme="outline">
|
|
||||||
<XIcon />
|
|
||||||
Clear filters
|
|
||||||
</Button> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -220,236 +288,60 @@ export default function Catalogue() {
|
|||||||
))}
|
))}
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
) : (
|
) : (
|
||||||
gamesWithRepacks.map((game, i) => (
|
results.map((game) => <GameItem key={game.id} game={game} />)
|
||||||
<button
|
)}
|
||||||
type="button"
|
|
||||||
key={i}
|
|
||||||
className="catalogue__game-item"
|
|
||||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: 200,
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
}}
|
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
|
||||||
alt={game.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
alignItems: "center",
|
||||||
alignItems: "flex-start",
|
justifyContent: "space-between",
|
||||||
gap: 4,
|
marginTop: 16,
|
||||||
padding: "16px 0",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{game.title}</span>
|
<span>{formatNumber(itemsCount)} resultados</span>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: vars.color.body,
|
|
||||||
marginBottom: 4,
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{game.genres?.join(", ")}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
<Pagination
|
||||||
{game.repacks.map((repack) => (
|
page={page}
|
||||||
<Badge key={repack}>{repack}</Badge>
|
totalPages={Math.ceil(itemsCount / PAGE_SIZE)}
|
||||||
))}
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
|
||||||
<Button theme="outline" key={i} onClick={() => setPage(i + 1)}>
|
|
||||||
{i + 1}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="catalogue__filters-container">
|
<div className="catalogue__filters-container">
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
|
{filterSections.map((section) => (
|
||||||
<FilterSection
|
<FilterSection
|
||||||
title={t("genres")}
|
key={section.key}
|
||||||
onClear={() => dispatch(setFilters({ genres: [] }))}
|
title={section.title}
|
||||||
color={filterCategoryColors.genres}
|
onClear={() => dispatch(setFilters({ [section.key]: [] }))}
|
||||||
|
color={filterCategoryColors[section.key]}
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
if (filters.genres.includes(value)) {
|
if (filters[section.key].includes(value)) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setFilters({
|
setFilters({
|
||||||
genres: filters.genres.filter((genre) => genre !== value),
|
[section.key]: filters[
|
||||||
})
|
section.key as
|
||||||
);
|
| "genres"
|
||||||
} else {
|
| "tags"
|
||||||
dispatch(setFilters({ genres: [...filters.genres, value] }));
|
| "downloadSourceFingerprints"
|
||||||
}
|
| "developers"
|
||||||
}}
|
| "publishers"
|
||||||
items={[
|
].filter((item) => item !== value),
|
||||||
"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),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterSection
|
|
||||||
title={t("tags")}
|
|
||||||
color={filterCategoryColors.tags}
|
|
||||||
onClear={() => 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
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterSection
|
|
||||||
title={t("download_sources")}
|
|
||||||
color={filterCategoryColors.downloadSourceFingerprints}
|
|
||||||
onClear={() =>
|
|
||||||
dispatch(setFilters({ downloadSourceFingerprints: [] }))
|
|
||||||
}
|
|
||||||
onSelect={(value) => {
|
|
||||||
if (filters.downloadSourceFingerprints.includes(value)) {
|
|
||||||
dispatch(
|
|
||||||
setFilters({
|
|
||||||
downloadSourceFingerprints:
|
|
||||||
filters.downloadSourceFingerprints.filter(
|
|
||||||
(fingerprint) => fingerprint !== value
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(
|
dispatch(
|
||||||
setFilters({
|
setFilters({
|
||||||
downloadSourceFingerprints: [
|
[section.key]: [...filters[section.key], value],
|
||||||
...filters.downloadSourceFingerprints,
|
|
||||||
value,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
items={downloadSources.map((downloadSource) => ({
|
items={section.items}
|
||||||
label: downloadSource.name,
|
|
||||||
value: downloadSource.fingerprint,
|
|
||||||
checked: filters.downloadSourceFingerprints.includes(
|
|
||||||
downloadSource.fingerprint
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterSection
|
|
||||||
title={t("developers")}
|
|
||||||
color={filterCategoryColors.developers}
|
|
||||||
onClear={() => 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),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterSection
|
|
||||||
title={t("publishers")}
|
|
||||||
color={filterCategoryColors.publishers}
|
|
||||||
onClear={() => 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),
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
50
src/renderer/src/pages/catalogue/filter-item.tsx
Normal file
50
src/renderer/src/pages/catalogue/filter-item.tsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
color: vars.color.body,
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
padding: "6px 12px",
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
backgroundColor: orbColor,
|
||||||
|
borderRadius: "50%",
|
||||||
|
marginRight: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{filter}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRemove}
|
||||||
|
style={{
|
||||||
|
color: vars.color.body,
|
||||||
|
marginLeft: 4,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
src/renderer/src/pages/catalogue/game-item.scss
Normal file
48
src/renderer/src/pages/catalogue/game-item.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
50
src/renderer/src/pages/catalogue/game-item.tsx
Normal file
50
src/renderer/src/pages/catalogue/game-item.tsx
Normal file
@ -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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="game-item"
|
||||||
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="game-item__cover"
|
||||||
|
src={steamUrlBuilder.library(game.objectId)}
|
||||||
|
alt={game.title}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="game-item__details">
|
||||||
|
<span>{game.title}</span>
|
||||||
|
<span className="game-item__genres">{game.genres?.join(", ")}</span>
|
||||||
|
|
||||||
|
<div className="game-item__repackers">
|
||||||
|
{uniqueRepackers.map((repacker) => (
|
||||||
|
<Badge key={repacker}>{repacker}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
83
src/renderer/src/pages/catalogue/pagination.tsx
Normal file
83
src/renderer/src/pages/catalogue/pagination.tsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Previous Button */}
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
style={{ width: 40 }}
|
||||||
|
disabled={page === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
justifyContent: "center",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16 }}>...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page Buttons */}
|
||||||
|
{Array.from(
|
||||||
|
{ length: endPage - startPage + 1 },
|
||||||
|
(_, i) => startPage + i
|
||||||
|
).map((pageNumber) => (
|
||||||
|
<Button
|
||||||
|
theme={page === pageNumber ? "primary" : "outline"}
|
||||||
|
key={pageNumber}
|
||||||
|
onClick={() => onPageChange(pageNumber)}
|
||||||
|
>
|
||||||
|
{pageNumber}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Next Button */}
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
style={{ width: 40 }}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -14,7 +14,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { useToast } from "@renderer/hooks";
|
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AxiosProgressEvent } from "axios";
|
import { AxiosProgressEvent } from "axios";
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
@ -145,6 +145,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
|
|
||||||
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
||||||
|
|
||||||
|
const userDetails = useAppSelector((state) => state.userDetails.userDetails);
|
||||||
|
const backupsPerGameLimit = userDetails?.quirks.backupsPerGameLimit ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@ -181,7 +184,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
disabled={
|
disabled={
|
||||||
disableActions ||
|
disableActions ||
|
||||||
!backupPreview?.overall.totalGames ||
|
!backupPreview?.overall.totalGames ||
|
||||||
artifacts.length >= 2
|
artifacts.length >= backupsPerGameLimit
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
@ -199,7 +202,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2>{t("backups")}</h2>
|
<h2>{t("backups")}</h2>
|
||||||
<small>{artifacts.length} / 2</small>
|
<small>
|
||||||
|
{artifacts.length} / {backupsPerGameLimit}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -42,3 +42,17 @@ export const downloadSourcesHeader = style({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -163,14 +163,8 @@ export function SettingsDownloadSources() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
className={styles.navigateToCatalogueButton}
|
||||||
display: "flex",
|
disabled={!downloadSource.fingerprint}
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
color: vars.color.muted,
|
|
||||||
textDecoration: "underline",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={() => navigateToCatalogue(downloadSource.fingerprint)}
|
onClick={() => navigateToCatalogue(downloadSource.fingerprint)}
|
||||||
>
|
>
|
||||||
<small>
|
<small>
|
||||||
|
@ -4,7 +4,7 @@ $dark-background-color: #151515;
|
|||||||
$muted-color: #c0c1c7;
|
$muted-color: #c0c1c7;
|
||||||
$body-color: #8e919b;
|
$body-color: #8e919b;
|
||||||
|
|
||||||
$border-color: #424244;
|
$border-color: rgba(255, 255, 255, 0.15);
|
||||||
$success-color: #1c9749;
|
$success-color: #1c9749;
|
||||||
$danger-color: #801d1e;
|
$danger-color: #801d1e;
|
||||||
$warning-color: #ffc107;
|
$warning-color: #ffc107;
|
||||||
|
@ -8,7 +8,7 @@ export const vars = createGlobalTheme(":root", {
|
|||||||
darkBackground: "#151515",
|
darkBackground: "#151515",
|
||||||
muted: "#c0c1c7",
|
muted: "#c0c1c7",
|
||||||
body: "#8e919b",
|
body: "#8e919b",
|
||||||
border: "#424244",
|
border: "rgba(255, 255, 255, 0.15)",
|
||||||
success: "#1c9749",
|
success: "#1c9749",
|
||||||
danger: "#e11d48",
|
danger: "#e11d48",
|
||||||
warning: "#ffc107",
|
warning: "#ffc107",
|
||||||
|
@ -243,6 +243,9 @@ export interface UserDetails {
|
|||||||
profileVisibility: ProfileVisibility;
|
profileVisibility: ProfileVisibility;
|
||||||
bio: string;
|
bio: string;
|
||||||
subscription: Subscription | null;
|
subscription: Subscription | null;
|
||||||
|
quirks: {
|
||||||
|
backupsPerGameLimit: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
@ -260,6 +263,9 @@ export interface UserProfile {
|
|||||||
currentGame: UserProfileCurrentGame | null;
|
currentGame: UserProfileCurrentGame | null;
|
||||||
bio: string;
|
bio: string;
|
||||||
hasActiveSubscription: boolean;
|
hasActiveSubscription: boolean;
|
||||||
|
quirks: {
|
||||||
|
backupsPerGameLimit: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
|
Loading…
Reference in New Issue
Block a user