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

@ -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}>
{
downloadSources.find(
(source) => source.fingerprint === fingerprint
)?.name
}
<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,15 +41,25 @@ export function FilterSection<T extends string | number>({
return (
<div>
<h3
style={{
fontSize: 16,
fontWeight: 500,
color: "#fff",
}}
>
{title}
</h3>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div
style={{
width: 10,
height: 10,
backgroundColor: color,
borderRadius: "50%",
}}
/>
<h3
style={{
fontSize: 16,
fontWeight: 500,
color: "#fff",
}}
>
{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