fix: fixing add game to library

This commit is contained in:
Hydra 2024-05-12 10:56:31 +01:00
commit 3bd8662b18
No known key found for this signature in database
34 changed files with 555 additions and 221 deletions

View file

@ -32,7 +32,7 @@ export function App({ children }: AppProps) {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary();
const { clearDownload, addPacket } = useDownload();
const { clearDownload, setLastPacket } = useDownload();
const dispatch = useAppDispatch();
@ -64,14 +64,14 @@ export function App({ children }: AppProps) {
return;
}
addPacket(downloadProgress);
setLastPacket(downloadProgress);
}
);
return () => {
unsubscribe();
};
}, [clearDownload, addPacket, updateLibrary]);
}, [clearDownload, setLastPacket, updateLibrary]);
const handleSearch = useCallback(
(query: string) => {

View file

@ -64,7 +64,7 @@ export function BottomPanel() {
<small>{status}</small>
</button>
<small>
<small tabIndex={0}>
v{version} &quot;{VERSION_CODENAME}&quot;
</small>
</footer>

View file

@ -19,6 +19,7 @@ const base = style({
":disabled": {
opacity: vars.opacity.disabled,
pointerEvents: "none",
cursor: "not-allowed",
},
});

View file

@ -7,3 +7,4 @@ export * from "./modal/modal";
export * from "./sidebar/sidebar";
export * from "./text-field/text-field";
export * from "./checkbox-field/checkbox-field";
export * from "./link/link";

View file

@ -0,0 +1,9 @@
import { style } from "@vanilla-extract/css";
export const link = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View file

@ -0,0 +1,33 @@
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
import cn from "classnames";
import * as styles from "./link.css";
export function Link({ children, to, className, ...props }: LinkProps) {
const openExternal = (event: React.MouseEvent) => {
event.preventDefault();
window.electron.openExternal(to as string);
};
if (typeof to === "string" && to.startsWith("http")) {
return (
<a
href={to}
className={cn(styles.link, className)}
onClick={openExternal}
{...props}
>
{children}
</a>
);
}
return (
<ReactRouterDomLink
className={cn(styles.link, className)}
to={to}
{...props}
>
{children}
</ReactRouterDomLink>
);
}

View file

@ -2,6 +2,13 @@ import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
export const textFieldContainer = style({
flex: "1",
gap: `${SPACING_UNIT}px`,
display: "flex",
flexDirection: "column",
});
export const textField = recipe({
base: {
display: "inline-flex",
@ -50,9 +57,3 @@ export const textFieldInput = style({
cursor: "text",
},
});
export const label = style({
marginBottom: `${SPACING_UNIT}px`,
display: "block",
color: vars.color.bodyText,
});

View file

@ -9,28 +9,31 @@ export interface TextFieldProps
> {
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
label?: string | React.ReactNode;
hint?: string | React.ReactNode;
textFieldProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
containerProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
}
export function TextField({
theme = "primary",
label,
hint,
textFieldProps,
containerProps,
...props
}: TextFieldProps) {
const [isFocused, setIsFocused] = useState(false);
const id = useId();
return (
<div style={{ flex: 1 }}>
{label && (
<label htmlFor={id} className={styles.label} tabIndex={0}>
{label}
</label>
)}
<div className={styles.textFieldContainer} {...containerProps}>
{label && <label tabIndex={0}>{label}</label>}
<div
className={styles.textField({ focused: isFocused, theme })}
@ -45,6 +48,8 @@ export function TextField({
{...props}
/>
</div>
{hint && <small tabIndex={0}>{hint}</small>}
</div>
);
}

View file

@ -3,13 +3,13 @@ import type { PayloadAction } from "@reduxjs/toolkit";
import type { TorrentProgress } from "@types";
interface DownloadState {
packets: TorrentProgress[];
lastPacket: TorrentProgress | null;
gameId: number | null;
gamesWithDeletionInProgress: number[];
}
const initialState: DownloadState = {
packets: [],
lastPacket: null,
gameId: null,
gamesWithDeletionInProgress: [],
};
@ -18,12 +18,12 @@ export const downloadSlice = createSlice({
name: "download",
initialState,
reducers: {
addPacket: (state, action: PayloadAction<TorrentProgress>) => {
state.packets = [...state.packets, action.payload];
setLastPacket: (state, action: PayloadAction<TorrentProgress>) => {
state.lastPacket = action.payload;
if (!state.gameId) state.gameId = action.payload.game.id;
},
clearDownload: (state) => {
state.packets = [];
state.lastPacket = null;
state.gameId = null;
},
setGameDeleting: (state, action: PayloadAction<number>) => {
@ -42,7 +42,7 @@ export const downloadSlice = createSlice({
});
export const {
addPacket,
setLastPacket,
clearDownload,
setGameDeleting,
removeGameFromDeleting,

View file

@ -1,6 +1,6 @@
import { formatDistance } from "date-fns";
import type { FormatDistanceOptions } from "date-fns";
import { ptBR, enUS, es, fr } from "date-fns/locale";
import { ptBR, enUS, es, fr, pl, hu, tr, ru, it } from "date-fns/locale";
import { useTranslation } from "react-i18next";
export function useDate() {
@ -10,6 +10,11 @@ export function useDate() {
if (i18n.language.startsWith("pt")) return ptBR;
if (i18n.language.startsWith("es")) return es;
if (i18n.language.startsWith("fr")) return fr;
if (i18n.language.startsWith("hu")) return hu;
if (i18n.language.startsWith("pl")) return pl;
if (i18n.language.startsWith("tr")) return tr;
if (i18n.language.startsWith("ru")) return ru;
if (i18n.language.startsWith("it")) return it;
return enUS;
};

View file

@ -4,7 +4,7 @@ import { formatDownloadProgress } from "@renderer/helpers";
import { useLibrary } from "./use-library";
import { useAppDispatch, useAppSelector } from "./redux";
import {
addPacket,
setLastPacket,
clearDownload,
setGameDeleting,
removeGameFromDeleting,
@ -18,13 +18,11 @@ export function useDownload() {
const { updateLibrary } = useLibrary();
const { formatDistance } = useDate();
const { packets, gamesWithDeletionInProgress } = useAppSelector(
const { lastPacket, gamesWithDeletionInProgress } = useAppSelector(
(state) => state.download
);
const dispatch = useAppDispatch();
const lastPacket = packets.at(-1);
const startDownload = (
repackId: number,
objectID: string,
@ -128,6 +126,6 @@ export function useDownload() {
deleteGame,
isGameDeleting,
clearDownload: () => dispatch(clearDownload()),
addPacket: (packet: TorrentProgress) => dispatch(addPacket(packet)),
setLastPacket: (packet: TorrentProgress) => dispatch(setLastPacket(packet)),
};
}

View file

@ -29,6 +29,7 @@ export const downloaderName = style({
borderRadius: "4px",
display: "flex",
alignItems: "center",
alignSelf: "flex-start",
});
export const downloads = style({

View file

@ -266,12 +266,11 @@ export function Downloads() {
>
{game.title}
</button>
<small className={styles.downloaderName}>
{downloaderName[game?.downloader]}
</small>
</div>
<small className={styles.downloaderName}>
{downloaderName[game?.downloader]}
</small>
{getGameInfo(game)}
</div>

View file

@ -20,6 +20,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
return gameDetails.screenshots.length;
}
}
return 0;
});

View file

@ -50,7 +50,7 @@ export function HeroPanelActions({
filters: [
{
name: "Game executable",
extensions: window.electron.platform === "win32" ? ["exe"] : [],
extensions: ["exe"],
},
],
})

View file

@ -36,7 +36,7 @@ export function RepacksModal({
useEffect(() => {
setFilteredRepacks(gameDetails.repacks);
}, [gameDetails.repacks]);
}, [gameDetails.repacks, visible]);
const handleRepackClick = (repack: GameRepack) => {
setRepack(repack);

View file

@ -17,11 +17,3 @@ export const hintText = style({
fontSize: "12px",
color: vars.color.bodyText,
});
export const settingsLink = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View file

@ -1,11 +1,10 @@
import { Button, Modal, TextField } from "@renderer/components";
import { Button, Link, Modal, TextField } from "@renderer/components";
import { GameRepack, ShopDetails } from "@types";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { formatBytes } from "@renderer/utils";
import { DiskSpace } from "check-disk-space";
import { Link } from "react-router-dom";
import * as styles from "./select-folder-modal.css";
import { DownloadIcon } from "@primer/octicons-react";
@ -100,10 +99,9 @@ export function SelectFolderModal({
</Button>
</div>
<p className={styles.hintText}>
{t("select_folder_hint")}{" "}
<Link to="/settings" className={styles.settingsLink}>
{t("settings")}
</Link>
<Trans i18nKey="select_folder_hint" ns="game_details">
<Link to="/settings" />
</Trans>
</p>
<Button onClick={handleStartClick} disabled={downloadStarting}>
<DownloadIcon />

View file

@ -0,0 +1,7 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css";
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});

View file

@ -0,0 +1,115 @@
import { useEffect, useState } from "react";
import { TextField, Button, CheckboxField } from "@renderer/components";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-general.css";
import type { UserPreferences } from "@types";
export interface SettingsGeneralProps {
userPreferences: UserPreferences | null;
updateUserPreferences: (values: Partial<UserPreferences>) => void;
}
export function SettingsGeneral({
userPreferences,
updateUserPreferences,
}: SettingsGeneralProps) {
const [form, setForm] = useState({
downloadsPath: "",
downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
});
useEffect(() => {
if (userPreferences) {
const {
downloadsPath,
downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled,
telemetryEnabled,
} = userPreferences;
window.electron.getDefaultDownloadsPath().then((defaultDownloadsPath) => {
setForm((prev) => ({
...prev,
downloadsPath: downloadsPath ?? defaultDownloadsPath,
downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled,
telemetryEnabled,
}));
});
}
}, [userPreferences]);
const { t } = useTranslation("settings");
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: form.downloadsPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
updateUserPreferences({ downloadsPath: path });
}
};
return (
<>
<div className={styles.downloadsPathField}>
<TextField
label={t("downloads_path")}
value={form.downloadsPath}
readOnly
disabled
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
>
{t("change")}
</Button>
</div>
<h3>{t("notifications")}</h3>
<CheckboxField
label={t("enable_download_notifications")}
checked={form.downloadNotificationsEnabled}
onChange={() =>
updateUserPreferences({
downloadNotificationsEnabled: !form.downloadNotificationsEnabled,
})
}
/>
<CheckboxField
label={t("enable_repack_list_notifications")}
checked={form.repackUpdatesNotificationsEnabled}
onChange={() =>
updateUserPreferences({
repackUpdatesNotificationsEnabled:
!form.repackUpdatesNotificationsEnabled,
})
}
/>
<h3>{t("telemetry")}</h3>
<CheckboxField
label={t("telemetry_description")}
checked={form.telemetryEnabled}
onChange={() =>
updateUserPreferences({
telemetryEnabled: !form.telemetryEnabled,
})
}
/>
</>
);
}

View file

@ -0,0 +1,9 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css";
export const form = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});

View file

@ -0,0 +1,83 @@
import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import * as styles from "./settings-real-debrid.css";
import type { UserPreferences } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
export interface SettingsRealDebridProps {
userPreferences: UserPreferences | null;
updateUserPreferences: (values: Partial<UserPreferences>) => void;
}
export function SettingsRealDebrid({
userPreferences,
updateUserPreferences,
}: SettingsRealDebridProps) {
const [form, setForm] = useState({
useRealDebrid: false,
realDebridApiToken: null as string | null,
});
const { t } = useTranslation("settings");
useEffect(() => {
if (userPreferences) {
setForm({
useRealDebrid: Boolean(userPreferences.realDebridApiToken),
realDebridApiToken: userPreferences.realDebridApiToken ?? null,
});
}
}, [userPreferences]);
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
updateUserPreferences({ realDebridApiToken: form.realDebridApiToken });
};
const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;
return (
<form className={styles.form} onSubmit={handleFormSubmit}>
<CheckboxField
label={t("enable_real_debrid")}
checked={form.useRealDebrid}
onChange={() =>
setForm((prev) => ({
...prev,
useRealDebrid: !form.useRealDebrid,
}))
}
/>
{form.useRealDebrid && (
<TextField
label={t("real_debrid_api_token_description")}
value={form.realDebridApiToken ?? ""}
type="password"
onChange={(event) =>
setForm({ ...form, realDebridApiToken: event.target.value })
}
placeholder="API Token"
containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }}
hint={
<Trans i18nKey="real_debrid_api_token_hint" ns="settings">
<Link to={REAL_DEBRID_API_TOKEN_URL} />
</Trans>
}
/>
)}
<Button
type="submit"
style={{ alignSelf: "flex-end" }}
disabled={isButtonDisabled}
>
Save changes
</Button>
</form>
);
}

View file

@ -20,11 +20,6 @@ export const content = style({
flexDirection: "column",
});
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const settingsCategories = style({
display: "flex",
gap: `${SPACING_UNIT}px`,

View file

@ -1,138 +1,46 @@
import { useEffect, useState } from "react";
import { Button, CheckboxField, TextField } from "@renderer/components";
import { Button, CheckboxField } from "@renderer/components";
import * as styles from "./settings.css";
import { useTranslation } from "react-i18next";
import { UserPreferences } from "@types";
import { SettingsRealDebrid } from "./settings-real-debrid";
import { SettingsGeneral } from "./settings-general";
const categories = ["general", "behavior", "real_debrid"];
export function Settings() {
const [currentCategory, setCurrentCategory] = useState(categories.at(0)!);
const [form, setForm] = useState({
downloadsPath: "",
downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
realDebridApiToken: null as string | null,
preferQuitInsteadOfHiding: false,
runAtStartup: false,
});
const [userPreferences, setUserPreferences] =
useState<UserPreferences | null>(null);
const { t } = useTranslation("settings");
useEffect(() => {
Promise.all([
window.electron.getDefaultDownloadsPath(),
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setForm({
downloadsPath: userPreferences?.downloadsPath || path,
downloadNotificationsEnabled:
userPreferences?.downloadNotificationsEnabled ?? false,
repackUpdatesNotificationsEnabled:
userPreferences?.repackUpdatesNotificationsEnabled ?? false,
telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
realDebridApiToken: userPreferences?.realDebridApiToken ?? null,
preferQuitInsteadOfHiding:
userPreferences?.preferQuitInsteadOfHiding ?? false,
runAtStartup: userPreferences?.runAtStartup ?? false,
});
window.electron.getUserPreferences().then((userPreferences) => {
setUserPreferences(userPreferences);
});
}, []);
const updateUserPreferences = <T extends keyof UserPreferences>(
field: T,
value: UserPreferences[T]
) => {
setForm((prev) => ({ ...prev, [field]: value }));
window.electron.updateUserPreferences({
[field]: value,
});
};
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: form.downloadsPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
updateUserPreferences("downloadsPath", path);
}
const handleUpdateUserPreferences = (values: Partial<UserPreferences>) => {
window.electron.updateUserPreferences(values);
};
const renderCategory = () => {
if (currentCategory === "general") {
return (
<>
<div className={styles.downloadsPathField}>
<TextField
label={t("downloads_path")}
value={form.downloadsPath}
readOnly
disabled
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
>
{t("change")}
</Button>
</div>
<h3>{t("notifications")}</h3>
<CheckboxField
label={t("enable_download_notifications")}
checked={form.downloadNotificationsEnabled}
onChange={() =>
updateUserPreferences(
"downloadNotificationsEnabled",
!form.downloadNotificationsEnabled
)
}
/>
<CheckboxField
label={t("enable_repack_list_notifications")}
checked={form.repackUpdatesNotificationsEnabled}
onChange={() =>
updateUserPreferences(
"repackUpdatesNotificationsEnabled",
!form.repackUpdatesNotificationsEnabled
)
}
/>
<h3>{t("telemetry")}</h3>
<CheckboxField
label={t("telemetry_description")}
checked={form.telemetryEnabled}
onChange={() =>
updateUserPreferences("telemetryEnabled", !form.telemetryEnabled)
}
/>
</>
<SettingsGeneral
userPreferences={userPreferences}
updateUserPreferences={handleUpdateUserPreferences}
/>
);
}
if (currentCategory === "real_debrid") {
return (
<TextField
label={t("real_debrid_api_token_description")}
value={form.realDebridApiToken ?? ""}
type="password"
onChange={(event) => {
updateUserPreferences("realDebridApiToken", event.target.value);
}}
placeholder="API Token"
<SettingsRealDebrid
userPreferences={userPreferences}
updateUserPreferences={handleUpdateUserPreferences}
/>
);
}
@ -177,7 +85,7 @@ export function Settings() {
))}
</section>
<h3>{t(currentCategory)}</h3>
<h2>{t(currentCategory)}</h2>
{renderCategory()}
</div>
</section>