mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding link direct from sources
This commit is contained in:
parent
3af0ae9f85
commit
d3450c5f65
11 changed files with 265 additions and 37 deletions
|
@ -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";
|
||||
|
|
|
@ -65,6 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
listener
|
||||
);
|
||||
},
|
||||
getPublishers: () => ipcRenderer.invoke("getPublishers"),
|
||||
getDevelopers: () => ipcRenderer.invoke("getDevelopers"),
|
||||
|
||||
/* User preferences */
|
||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
},
|
||||
|
|
2
src/renderer/src/declaration.d.ts
vendored
2
src/renderer/src/declaration.d.ts
vendored
|
@ -68,6 +68,8 @@ declare global {
|
|||
shop: GameShop,
|
||||
cb: (achievements: GameAchievement[]) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
getPublishers: () => Promise<string[]>;
|
||||
getDevelopers: () => Promise<string[]>;
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
|
|
|
@ -34,4 +34,4 @@ export const catalogueSearchSlice = createSlice({
|
|||
},
|
||||
});
|
||||
|
||||
export const { setSearch } = catalogueSearchSlice.actions;
|
||||
export const { setSearch, clearSearch } = catalogueSearchSlice.actions;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue