mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding import download source
This commit is contained in:
parent
ddd9ea69df
commit
48e07370e4
70 changed files with 925 additions and 1261 deletions
|
@ -23,6 +23,7 @@ export const heroMedia = style({
|
|||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "all ease 0.2s",
|
||||
imageRendering: "revert",
|
||||
selectors: {
|
||||
[`${hero}:hover &`]: {
|
||||
transform: "scale(1.02)",
|
||||
|
|
|
@ -21,7 +21,7 @@ export const modal = recipe({
|
|||
animationName: fadeIn,
|
||||
animationDuration: "0.3s",
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "5px",
|
||||
borderRadius: "4px",
|
||||
maxWidth: "600px",
|
||||
color: vars.color.body,
|
||||
maxHeight: "100%",
|
||||
|
|
|
@ -42,18 +42,33 @@ export const textField = recipe({
|
|||
},
|
||||
});
|
||||
|
||||
export const textFieldInput = style({
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
outline: "none",
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
textOverflow: "ellipsis",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
":focus": {
|
||||
cursor: "text",
|
||||
export const textFieldInput = recipe({
|
||||
base: {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
outline: "none",
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
textOverflow: "ellipsis",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
":focus": {
|
||||
cursor: "text",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
readOnly: {
|
||||
true: {
|
||||
textOverflow: "inherit",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const togglePasswordButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.muted,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useId, useState } from "react";
|
||||
import { useId, useMemo, useState } from "react";
|
||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||
import * as styles from "./text-field.css";
|
||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface TextFieldProps
|
||||
extends React.DetailedHTMLProps<
|
||||
|
@ -28,9 +30,20 @@ export function TextField({
|
|||
containerProps,
|
||||
...props
|
||||
}: TextFieldProps) {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const id = useId();
|
||||
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const { t } = useTranslation("forms");
|
||||
|
||||
const showPasswordToggleButton = props.type === "password";
|
||||
|
||||
const inputType = useMemo(() => {
|
||||
if (props.type === "password" && isPasswordVisible) return "text";
|
||||
return props.type ?? "text";
|
||||
}, [props.type, isPasswordVisible]);
|
||||
|
||||
return (
|
||||
<div className={styles.textFieldContainer} {...containerProps}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
|
@ -41,12 +54,27 @@ export function TextField({
|
|||
>
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
className={styles.textFieldInput}
|
||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
{...props}
|
||||
type={inputType}
|
||||
/>
|
||||
|
||||
{showPasswordToggleButton && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.togglePasswordButton}
|
||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
aria-label={t("toggle_password_visibility")}
|
||||
>
|
||||
{isPasswordVisible ? (
|
||||
<EyeClosedIcon size={16} />
|
||||
) : (
|
||||
<EyeIcon size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hint && <small>{hint}</small>}
|
||||
|
|
13
src/renderer/src/declaration.d.ts
vendored
13
src/renderer/src/declaration.d.ts
vendored
|
@ -12,6 +12,7 @@ import type {
|
|||
UserPreferences,
|
||||
StartGameDownloadPayload,
|
||||
RealDebridUser,
|
||||
DownloadSource,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
|
@ -33,7 +34,7 @@ declare global {
|
|||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||
getCatalogue: (category: CatalogueCategory) => Promise<CatalogueEntry[]>;
|
||||
getCatalogue: () => Promise<CatalogueEntry[]>;
|
||||
getGameShopDetails: (
|
||||
objectID: string,
|
||||
shop: GameShop,
|
||||
|
@ -58,7 +59,7 @@ declare global {
|
|||
shop: GameShop,
|
||||
executablePath: string | null
|
||||
) => Promise<void>;
|
||||
getLibrary: () => Promise<Game[]>;
|
||||
getLibrary: () => Promise<Omit<Game, "repacks">[]>;
|
||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||
closeGame: (gameId: number) => Promise<boolean>;
|
||||
|
@ -77,6 +78,14 @@ declare global {
|
|||
autoLaunch: (enabled: boolean) => Promise<void>;
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
|
||||
/* Download sources */
|
||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||
validateDownloadSource: (
|
||||
url: string
|
||||
) => Promise<{ name: string; downloadCount: number }>;
|
||||
addDownloadSource: (url: string) => Promise<DownloadSource>;
|
||||
removeDownloadSource: (id: number) => Promise<void>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
|
||||
|
|
|
@ -40,3 +40,7 @@ export const buildGameDetailsPath = (
|
|||
const searchParams = new URLSearchParams({ title: game.title, ...params });
|
||||
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
export const numberFormatter = new Intl.NumberFormat("en-US", {
|
||||
maximumSignificantDigits: 3,
|
||||
});
|
||||
|
|
|
@ -99,12 +99,7 @@ export function Downloads() {
|
|||
}
|
||||
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<p>{game.repack?.title}</p>
|
||||
<p>{t("completed")}</p>
|
||||
</>
|
||||
);
|
||||
return <p>{t("completed")}</p>;
|
||||
}
|
||||
|
||||
if (game.status === "paused") {
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface GameDetailsContext {
|
|||
game: Game | null;
|
||||
shopDetails: ShopDetails | null;
|
||||
repacks: GameRepack[];
|
||||
shop: GameShop;
|
||||
gameTitle: string;
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
|
@ -35,6 +36,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||
game: null,
|
||||
shopDetails: null,
|
||||
repacks: [],
|
||||
shop: "steam",
|
||||
gameTitle: "",
|
||||
isGameRunning: false,
|
||||
isLoading: false,
|
||||
|
@ -92,7 +94,7 @@ export function GameDetailsContextProvider({
|
|||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
Promise.allSettled([
|
||||
window.electron.getGameShopDetails(
|
||||
objectID!,
|
||||
shop as GameShop,
|
||||
|
@ -100,9 +102,12 @@ export function GameDetailsContextProvider({
|
|||
),
|
||||
window.electron.searchGameRepacks(gameTitle),
|
||||
])
|
||||
.then(([appDetails, repacks]) => {
|
||||
if (appDetails) setGameDetails(appDetails);
|
||||
setRepacks(repacks);
|
||||
.then(([appDetailsResult, repacksResult]) => {
|
||||
if (appDetailsResult.status === "fulfilled")
|
||||
setGameDetails(appDetailsResult.value);
|
||||
|
||||
if (repacksResult.status === "fulfilled")
|
||||
setRepacks(repacksResult.value);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
|
@ -174,6 +179,7 @@ export function GameDetailsContextProvider({
|
|||
value={{
|
||||
game,
|
||||
shopDetails,
|
||||
shop: shop as GameShop,
|
||||
repacks,
|
||||
gameTitle,
|
||||
isGameRunning,
|
||||
|
|
|
@ -46,7 +46,9 @@ export function DODIInstallationGuide({
|
|||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<p style={{ fontFamily: "Fira Sans", marginBottom: 8 }}>
|
||||
<p
|
||||
style={{ fontFamily: "Fira Sans", marginBottom: `${SPACING_UNIT}px` }}
|
||||
>
|
||||
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
|
||||
<ArrowUpIcon size={16} />
|
||||
</Trans>
|
||||
|
|
|
@ -6,6 +6,7 @@ export const contentSidebar = style({
|
|||
borderLeft: `solid 1px ${vars.color.border};`,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
width: "100%",
|
||||
|
@ -86,6 +87,14 @@ export const howLongToBeatCategorySkeleton = style({
|
|||
height: "76px",
|
||||
});
|
||||
|
||||
export const technicalDetailsContainer = style({
|
||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||
color: vars.color.body,
|
||||
userSelect: "text",
|
||||
position: "absolute",
|
||||
bottom: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
globalStyle(`${requirementsDetails} a`, {
|
||||
display: "flex",
|
||||
color: vars.color.body,
|
||||
|
|
|
@ -16,7 +16,8 @@ export function Sidebar() {
|
|||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext);
|
||||
const { gameTitle, shopDetails, shop, objectID } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
|
@ -74,6 +75,15 @@ export function Sidebar() {
|
|||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={styles.technicalDetailsContainer}>
|
||||
<p>
|
||||
<small>shop: "{shop}"</small>
|
||||
</p>
|
||||
<p>
|
||||
<small>objectID: "{objectID}"</small>
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const homeCategories = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const homeHeader = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||
|
||||
import { Button, GameCard, Hero } from "@renderer/components";
|
||||
import {
|
||||
Steam250Game,
|
||||
type CatalogueCategory,
|
||||
type CatalogueEntry,
|
||||
} from "@types";
|
||||
import type { Steam250Game, CatalogueEntry } from "@types";
|
||||
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
|
||||
|
@ -18,8 +14,6 @@ import { vars } from "../../theme.css";
|
|||
import Lottie from "lottie-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
const categories: CatalogueCategory[] = ["trending", "recently_added"];
|
||||
|
||||
export function Home() {
|
||||
const { t } = useTranslation("home");
|
||||
const navigate = useNavigate();
|
||||
|
@ -27,22 +21,15 @@ export function Home() {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const [catalogue, setCatalogue] = useState<CatalogueEntry[]>([]);
|
||||
|
||||
const [catalogue, setCatalogue] = useState<
|
||||
Record<CatalogueCategory, CatalogueEntry[]>
|
||||
>({
|
||||
trending: [],
|
||||
recently_added: [],
|
||||
});
|
||||
|
||||
const getCatalogue = useCallback((category: CatalogueCategory) => {
|
||||
const getCatalogue = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
window.electron
|
||||
.getCatalogue(category)
|
||||
.getCatalogue()
|
||||
.then((catalogue) => {
|
||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
||||
setCatalogue(catalogue);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
|
@ -50,15 +37,6 @@ export function Home() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const currentCategory = searchParams.get("category") || categories[0];
|
||||
|
||||
const handleSelectCategory = (category: CatalogueCategory) => {
|
||||
if (category !== currentCategory) {
|
||||
getCatalogue(category);
|
||||
navigate(`/?category=${category}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getRandomGame = useCallback(() => {
|
||||
window.electron.getRandomGame().then((game) => {
|
||||
if (game) setRandomGame(game);
|
||||
|
@ -80,9 +58,10 @@ export function Home() {
|
|||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
getCatalogue(currentCategory as CatalogueCategory);
|
||||
getCatalogue();
|
||||
|
||||
getRandomGame();
|
||||
}, [getCatalogue, currentCategory, getRandomGame]);
|
||||
}, [getCatalogue, getRandomGame]);
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
|
@ -92,17 +71,7 @@ export function Home() {
|
|||
<Hero />
|
||||
|
||||
<section className={styles.homeHeader}>
|
||||
<div className={styles.homeCategories}>
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={currentCategory === category ? "primary" : "outline"}
|
||||
onClick={() => handleSelectCategory(category)}
|
||||
>
|
||||
{t(category)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<h2>{t("trending")}</h2>
|
||||
|
||||
<Button
|
||||
onClick={handleRandomizerClick}
|
||||
|
@ -120,14 +89,12 @@ export function Home() {
|
|||
</Button>
|
||||
</section>
|
||||
|
||||
<h2>{t(currentCategory)}</h2>
|
||||
|
||||
<section className={styles.cards}>
|
||||
{isLoading
|
||||
? Array.from({ length: 12 }).map((_, index) => (
|
||||
<Skeleton key={index} className={styles.cardSkeleton} />
|
||||
))
|
||||
: catalogue[currentCategory as CatalogueCategory].map((result) => (
|
||||
: catalogue.map((result) => (
|
||||
<GameCard
|
||||
key={result.objectID}
|
||||
game={result}
|
||||
|
|
112
src/renderer/src/pages/settings/add-download-source-modal.tsx
Normal file
112
src/renderer/src/pages/settings/add-download-source-modal.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./settings-download-sources.css";
|
||||
import { numberFormatter } from "@renderer/helpers";
|
||||
|
||||
interface AddDownloadSourceModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onAddDownloadSource: () => void;
|
||||
}
|
||||
|
||||
export function AddDownloadSourceModal({
|
||||
visible,
|
||||
onClose,
|
||||
onAddDownloadSource,
|
||||
}: AddDownloadSourceModalProps) {
|
||||
const [value, setValue] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [validationResult, setValidationResult] = useState<{
|
||||
name: string;
|
||||
downloadCount: number;
|
||||
} | null>(null);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const handleValidateDownloadSource = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await window.electron.validateDownloadSource(value);
|
||||
setValidationResult(result);
|
||||
console.log(result);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddDownloadSource = async () => {
|
||||
await window.electron.addDownloadSource(value);
|
||||
onClose();
|
||||
onAddDownloadSource();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("add_download_source")}
|
||||
description={t("add_download_source_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
minWidth: "500px",
|
||||
}}
|
||||
>
|
||||
<div className={styles.downloadSourceField}>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
placeholder="Insert a valid JSON url"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={handleValidateDownloadSource}
|
||||
disabled={isLoading || !value}
|
||||
>
|
||||
{t("validate_download_source")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{validationResult && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: `${SPACING_UNIT * 3}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
>
|
||||
<h4>{validationResult?.name}</h4>
|
||||
<small>
|
||||
Found {numberFormatter.format(validationResult?.downloadCount)}{" "}
|
||||
download options
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<Button type="button" onClick={handleAddDownloadSource}>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const downloadSourceField = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloadSourceItem = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
borderRadius: "8px",
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
export const downloadSourceItemHeader = style({
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
});
|
102
src/renderer/src/pages/settings/settings-download-sources.tsx
Normal file
102
src/renderer/src/pages/settings/settings-download-sources.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TextField, Button } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./settings-download-sources.css";
|
||||
import type { DownloadSource } from "@types";
|
||||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||
import { AddDownloadSourceModal } from "./add-download-source-modal";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { numberFormatter } from "@renderer/helpers";
|
||||
|
||||
export function SettingsDownloadSources() {
|
||||
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||
useState(false);
|
||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const getDownloadSources = async () => {
|
||||
return window.electron.getDownloadSources().then((sources) => {
|
||||
setDownloadSources(sources);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDownloadSources();
|
||||
}, []);
|
||||
|
||||
const handleRemoveSource = async (id: number) => {
|
||||
await window.electron.removeDownloadSource(id);
|
||||
showSuccessToast("Removed download source");
|
||||
|
||||
getDownloadSources();
|
||||
};
|
||||
|
||||
const handleAddDownloadSource = async () => {
|
||||
await getDownloadSources();
|
||||
showSuccessToast("Download source successfully added");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddDownloadSourceModal
|
||||
visible={showAddDownloadSourceModal}
|
||||
onClose={() => setShowAddDownloadSourceModal(false)}
|
||||
onAddDownloadSource={handleAddDownloadSource}
|
||||
/>
|
||||
|
||||
<p style={{ fontFamily: '"Fira Sans"' }}>
|
||||
{t("download_sources_description")}
|
||||
</p>
|
||||
|
||||
{downloadSources.map((downloadSource) => (
|
||||
<div key={downloadSource.id} className={styles.downloadSourceItem}>
|
||||
<div className={styles.downloadSourceItemHeader}>
|
||||
<h3>{downloadSource.name}</h3>
|
||||
<small>
|
||||
{t("download_options", {
|
||||
count: downloadSource.repackCount,
|
||||
countFormatted: numberFormatter.format(
|
||||
downloadSource.repackCount
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className={styles.downloadSourceField}>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
value={downloadSource.url}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||
>
|
||||
<NoEntryIcon />
|
||||
{t("remove_download_source")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||
>
|
||||
<PlusCircleIcon />
|
||||
{t("add_download_source")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -9,13 +9,19 @@ import { SettingsGeneral } from "./settings-general";
|
|||
import { SettingsBehavior } from "./settings-behavior";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { setUserPreferences } from "@renderer/features";
|
||||
import { SettingsDownloadSources } from "./settings-download-sources";
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const categories = [t("general"), t("behavior"), "Real-Debrid"];
|
||||
const categories = [
|
||||
t("general"),
|
||||
t("behavior"),
|
||||
t("download_sources"),
|
||||
"Real-Debrid",
|
||||
];
|
||||
|
||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||
|
||||
|
@ -41,6 +47,10 @@ export function Settings() {
|
|||
);
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 2) {
|
||||
return <SettingsDownloadSources />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsRealDebrid updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
|
|
|
@ -6,10 +6,10 @@ interface BinaryNotFoundModalProps {
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const BinaryNotFoundModal = ({
|
||||
export function BinaryNotFoundModal({
|
||||
visible,
|
||||
onClose,
|
||||
}: BinaryNotFoundModalProps) => {
|
||||
}: BinaryNotFoundModalProps) {
|
||||
const { t } = useTranslation("binary_not_found_modal");
|
||||
|
||||
return (
|
||||
|
@ -22,4 +22,4 @@ export const BinaryNotFoundModal = ({
|
|||
{t("instructions")}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue