feat: adding link direct from sources

This commit is contained in:
Chubby Granny Chaser 2024-12-20 21:28:51 +00:00
parent 3af0ae9f85
commit d3450c5f65
No known key found for this signature in database
11 changed files with 265 additions and 37 deletions

View File

@ -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",

View File

@ -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";

View File

@ -65,6 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
listener
);
},
getPublishers: () => ipcRenderer.invoke("getPublishers"),
getDevelopers: () => ipcRenderer.invoke("getDevelopers"),
/* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),

View File

@ -7,9 +7,5 @@ export interface BadgeProps {
}
export function Badge({ children }: BadgeProps) {
return (
<div className="badge">
<span>{children}</span>
</div>
);
return <div className="badge">{children}</div>;
}

View File

@ -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)",
},

View File

@ -68,6 +68,8 @@ declare global {
shop: GameShop,
cb: (achievements: GameAchievement[]) => void
) => () => Electron.IpcRenderer;
getPublishers: () => Promise<string[]>;
getDevelopers: () => Promise<string[]>;
/* Library */
addGameToLibrary: (

View File

@ -34,4 +34,4 @@ export const catalogueSearchSlice = createSlice({
},
});
export const { setSearch } = catalogueSearchSlice.actions;
export const { setSearch, clearSearch } = catalogueSearchSlice.actions;

View File

@ -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<HTMLInputElement>(null);
@ -34,6 +42,8 @@ export default function Catalogue() {
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const [games, setGames] = useState<any[]>([]);
const [publishers, setPublishers] = useState<string[]>([]);
const [developers, setDevelopers] = useState<string[]>([]);
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() {
}}
>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{filters.genres.map((genre) => (
<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>
))}
{filters.tags.map((tag) => (
<Badge key={tag}>
<div style={{ display: "flex", gap: 4, alignItems: "center" }}>
{tag}
</div>
</Badge>
))}
{filters.downloadSourceFingerprints.map((fingerprint) => (
<Badge key={fingerprint}>
<div style={{ display: "flex", gap: 4, alignItems: "center" }}>
<div
style={{
width: 10,
height: 10,
backgroundColor:
filterCategoryColors.downloadSourceFingerprints,
borderRadius: "50%",
}}
/>
{
downloadSources.find(
(source) => source.fingerprint === fingerprint
)?.name
}
</div>
</Badge>
))}
</div>
@ -248,6 +305,7 @@ export default function Catalogue() {
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<FilterSection
title="Genres"
color={filterCategoryColors.genres}
onSelect={(value) => {
if (filters.genres.includes(value)) {
dispatch(
@ -300,6 +358,7 @@ export default function Catalogue() {
<FilterSection
title="User tags"
color={filterCategoryColors.tags}
onSelect={(value) => {
if (filters.tags.includes(value)) {
dispatch(
@ -322,6 +381,7 @@ export default function Catalogue() {
<FilterSection
title="Download sources"
color={filterCategoryColors.downloadSourceFingerprints}
onSelect={(value) => {
if (filters.downloadSourceFingerprints.includes(value)) {
dispatch(
@ -351,6 +411,56 @@ export default function Catalogue() {
),
}))}
/>
<FilterSection
title="Developers"
color={filterCategoryColors.developers}
onSelect={(value) => {
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),
}))}
/>
<FilterSection
title="Publishers"
color={filterCategoryColors.publishers}
onSelect={(value) => {
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),
}))}
/>
</div>
</div>
</div>

View File

@ -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<T extends string | number> {
title: string;
items: {
@ -10,11 +12,13 @@ export interface FilterSectionProps<T extends string | number> {
checked: boolean;
}[];
onSelect: (value: T) => void;
color: string;
}
export function FilterSection<T extends string | number>({
title,
items,
color,
onSelect,
}: FilterSectionProps<T>) {
const [search, setSearch] = useState("");
@ -37,6 +41,15 @@ export function FilterSection<T extends string | number>({
return (
<div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div
style={{
width: 10,
height: 10,
backgroundColor: color,
borderRadius: "50%",
}}
/>
<h3
style={{
fontSize: 16,
@ -46,6 +59,7 @@ export function FilterSection<T extends string | number>({
>
{title}
</h3>
</div>
<span style={{ fontSize: 12, marginBottom: 12, display: "block" }}>
{formatNumber(items.length)} disponíveis
@ -59,25 +73,31 @@ export function FilterSection<T extends string | number>({
theme="dark"
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 8,
overflowY: "auto",
maxHeight: 28 * 10,
<List
data={filteredItems}
height={28 * 10}
itemHeight={28}
itemKey="value"
styles={{
verticalScrollBar: {
backgroundColor: "rgba(255, 255, 255, 0.03)",
},
verticalScrollBarThumb: {
backgroundColor: "rgba(255, 255, 255, 0.08)",
borderRadius: "24px",
},
}}
>
{filteredItems.map((item) => (
<div key={item.value}>
{(item) => (
<div key={item.value} style={{ height: 28, maxHeight: 28 }}>
<CheckboxField
label={item.label}
checked={item.checked}
onChange={() => onSelect(item.value)}
/>
</div>
))}
</div>
)}
</List>
</div>
);
}

View File

@ -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 (
<>
<AddDownloadSourceModal
@ -147,12 +160,17 @@ export function SettingsDownloadSources() {
<Badge>{statusTitle[downloadSource.status]}</Badge>
</div>
<div
<button
type="button"
style={{
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
color: vars.color.muted,
textDecoration: "underline",
cursor: "pointer",
}}
onClick={() => navigateToCatalogue(downloadSource.fingerprint)}
>
<small>
{t("download_count", {
@ -161,7 +179,7 @@ export function SettingsDownloadSources() {
downloadSource.downloadCount.toLocaleString(),
})}
</small>
</div>
</button>
</div>
<TextField

View File

@ -978,6 +978,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.24.0":
version "7.24.0"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz"
@ -3852,7 +3859,7 @@ ci-info@^3.2.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
classnames@^2.5.1:
classnames@^2.2.1, classnames@^2.2.6, classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
@ -3924,6 +3931,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^1.0.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -4384,6 +4396,14 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-helpers@^5.1.3:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
@ -7248,7 +7268,7 @@ promise-retry@^2.0.1:
err-code "^2.0.2"
retry "^0.12.0"
prop-types@^15.8.1:
prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -7300,6 +7320,34 @@ quick-lru@^5.1.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
rc-resize-observer@^1.0.0:
version "1.4.3"
resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz#4fd41fa561ba51362b5155a07c35d7c89a1ea569"
integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==
dependencies:
"@babel/runtime" "^7.20.7"
classnames "^2.2.1"
rc-util "^5.44.1"
resize-observer-polyfill "^1.5.1"
rc-util@^5.36.0, rc-util@^5.44.1:
version "5.44.2"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.44.2.tgz#6bc5db0e96ebdb515eb5977a7371887e5413a6f8"
integrity sha512-uGSk3hpPBLa3/0QAcKhCjgl4SFnhQCJDLvvpoLdbR6KgDuXrujG+dQaUeUvBJr2ZWak1O/9n+cYbJiWmmk95EQ==
dependencies:
"@babel/runtime" "^7.18.3"
react-is "^18.2.0"
rc-virtual-list@^3.16.1:
version "3.16.1"
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.16.1.tgz#073d75cc0295497cdd9a35d6f5d1b71b4f35233e"
integrity sha512-algM5UsB7vrlPNr9lsZEH8s9KHkP8XbT/Y0qylyPkiM8mIOlSJLjBNADcmbYPEQCm4zW82mZRJuVHNzqqN0EAQ==
dependencies:
"@babel/runtime" "^7.20.0"
classnames "^2.2.6"
rc-resize-observer "^1.0.0"
rc-util "^5.36.0"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@ -7336,6 +7384,16 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-loading-skeleton@^3.4.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz#da2090355b4dedcad5c53cb3f0ed364e3a76d6ca"
@ -7369,6 +7427,18 @@ react-router@6.26.2:
dependencies:
"@remix-run/router" "1.19.2"
react-virtualized@^9.22.5:
version "9.22.5"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.5.tgz#bfb96fed519de378b50d8c0064b92994b3b91620"
integrity sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==
dependencies:
"@babel/runtime" "^7.7.2"
clsx "^1.0.4"
dom-helpers "^5.1.3"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"
react@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
@ -7485,6 +7555,11 @@ reselect@^5.1.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-alpn@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"