) => {
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default MainContainer;
19 |
--------------------------------------------------------------------------------
/src/components/folderButton.tsx:
--------------------------------------------------------------------------------
1 | import { invoke } from "@/lib";
2 | import { Folder } from "lucide-react";
3 | import { JSX } from "react";
4 | import { Button } from "./ui/button";
5 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
6 |
7 | interface Props {
8 | path: string | "downloads";
9 | icon?: JSX.Element;
10 | variant?:
11 | | "default"
12 | | "destructive"
13 | | "outline"
14 | | "secondary"
15 | | "ghost"
16 | | "link"
17 | | null;
18 | size?: "default" | "icon" | "sm" | "lg" | null;
19 | tooltip?: string;
20 | }
21 |
22 | const FolderButton = ({
23 | path,
24 | icon,
25 | tooltip,
26 | variant,
27 | size = "icon",
28 | }: Props) => {
29 | const handleClick = () => {
30 | if (path === "downloads") {
31 | invoke("generic:open-downloads");
32 | return;
33 | }
34 | invoke("generic:open-folder", path);
35 | };
36 |
37 | return (
38 |
39 |
40 |
43 |
44 | {tooltip}
45 |
46 | );
47 | };
48 |
49 | export default FolderButton;
50 |
--------------------------------------------------------------------------------
/src/components/genericRow.tsx:
--------------------------------------------------------------------------------
1 | import { igdb } from "@/lib";
2 | import { useQuery } from "@tanstack/react-query";
3 | import DefaultCard from "./cards/defaultCard";
4 | import GenericRowSkeleton from "./skeletons/genericRow";
5 | import { CarouselContent, CarouselItem } from "./ui/carousel";
6 |
7 | interface GenericRowProps {
8 | dataToFetch: "mostAnticipated" | "topRated" | "newReleases";
9 | fetchKey: string[];
10 | }
11 |
12 | const GenericRow = ({ dataToFetch, fetchKey }: GenericRowProps) => {
13 | const fetcher = async () => {
14 | const data = await igdb[dataToFetch]();
15 | return data;
16 | };
17 |
18 | const { data, isPending, error } = useQuery({
19 | queryKey: ["igdb", ...fetchKey],
20 | queryFn: fetcher,
21 | });
22 |
23 | if (isPending) return ;
24 | if (error) return null;
25 |
26 | return (
27 |
28 | {!!data?.length &&
29 | data?.map((game) => (
30 |
35 |
36 |
37 | ))}
38 |
39 | );
40 | };
41 |
42 | export default GenericRow;
43 |
--------------------------------------------------------------------------------
/src/components/info/infoBar.tsx:
--------------------------------------------------------------------------------
1 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
2 | import TopbarSkeleton from "../skeletons/info/topbar.skeleton";
3 | import { Topbar } from "./topbar";
4 |
5 | interface InfoBarProps {
6 | onBack: () => void;
7 | titleText: string;
8 | data?: IGDBReturnDataType;
9 | isPending: boolean;
10 | }
11 |
12 | export const InfoBar = ({
13 | onBack,
14 | titleText,
15 | isPending,
16 | data,
17 | }: InfoBarProps) => {
18 | if (isPending) return ;
19 | if (!data) return null;
20 |
21 | return (
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/info/media/index.tsx:
--------------------------------------------------------------------------------
1 | import { useLanguageContext } from "@/contexts/I18N";
2 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
3 | import MediaScreenshots from "./screenshots";
4 | import MediaTrailer from "./trailer";
5 | import { H1 } from "@/components/ui/typography";
6 |
7 | const GameMedia = (props: IGDBReturnDataType) => {
8 | const { t } = useLanguageContext();
9 | const { name, screenshots, videos } = props;
10 |
11 | return (
12 |
13 |
{t("media")}
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default GameMedia;
23 |
--------------------------------------------------------------------------------
/src/components/info/media/trailer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib";
2 | import { Video } from "@/lib/api/igdb/types";
3 |
4 | interface MediaTrailerProps {
5 | videos: Video[] | undefined;
6 | className?: string;
7 | }
8 |
9 | const MediaTrailer = ({ videos, className }: MediaTrailerProps) => {
10 | if (!videos?.length) return null;
11 |
12 | return (
13 |
20 | );
21 | };
22 |
23 | export default MediaTrailer;
24 |
--------------------------------------------------------------------------------
/src/components/info/specs/index.tsx:
--------------------------------------------------------------------------------
1 | import RequirementsRow from "@/components/info/specs/row";
2 | import { H5 } from "@/components/ui/typography";
3 | import { useLanguageContext } from "@/contexts/I18N";
4 | import { PcRequirements } from "@/lib/api/igdb/types";
5 | import { useMemo } from "react";
6 |
7 | type PcSpecsProps = PcRequirements;
8 |
9 | export type Data = {
10 | type: "minimum" | "recommended";
11 | data: string | undefined | null;
12 | };
13 |
14 | const PcSpecs = ({ minimum, recommended }: PcSpecsProps) => {
15 | const { t } = useLanguageContext();
16 |
17 | const items = useMemo(() => {
18 | const data: Data[] | null = [
19 | { type: "minimum", data: minimum },
20 | { type: "recommended", data: recommended },
21 | ];
22 |
23 | return data.filter((item) => item?.data);
24 | }, [minimum, recommended]);
25 |
26 | if (!items?.length) return null;
27 |
28 | return (
29 |
30 |
{t("system_requirements")}
31 |
32 | {items.map((item) => (
33 |
38 | ))}
39 |
40 |
41 | );
42 | };
43 |
44 | export default PcSpecs;
45 |
--------------------------------------------------------------------------------
/src/components/info/specs/row.tsx:
--------------------------------------------------------------------------------
1 | import { Data } from "@/components/info/specs";
2 | import {
3 | H4,
4 | TypographyMuted,
5 | TypographySmall,
6 | } from "@/components/ui/typography";
7 |
8 | import { useLanguageContext } from "@/contexts/I18N";
9 | import { scrapeOptions } from "@/lib";
10 | import { useMemo } from "react";
11 |
12 | type RequirementsRowProps = Data;
13 |
14 | const RequirementsRow = ({ type, data }: RequirementsRowProps) => {
15 | const { t } = useLanguageContext();
16 | const specs = useMemo(() => !!data && scrapeOptions(data), [data]);
17 |
18 | const isSpecsEm = useMemo(() => Object.values(specs).length, [specs]);
19 |
20 | if (!data) return null;
21 | if (!isSpecsEm) return null;
22 |
23 | return (
24 |
25 |
{t(type?.toLowerCase())}
26 |
27 | {Object.entries(specs).map(([key, value]) => (
28 |
29 | {key}
30 | {value[0]}
31 |
32 | ))}
33 |
34 | );
35 | };
36 |
37 | export default RequirementsRow;
38 |
--------------------------------------------------------------------------------
/src/components/info/tabs/selected.tsx:
--------------------------------------------------------------------------------
1 | import { InfoItadProps, InfoProps } from "@/@types";
2 | import { InfoReturn, ReleaseDate } from "@/lib/api/igdb/types";
3 | import PcSpecs from "../specs";
4 | import InfoAboutTab from "./about";
5 |
6 | type SelectedTab0Data = InfoItadProps &
7 | InfoProps & {
8 | data: InfoReturn | undefined;
9 | isReleased: boolean;
10 | releaseDate: ReleaseDate | null | undefined;
11 | };
12 |
13 | type Props = SelectedTab0Data & {
14 | selectedTab: number;
15 | };
16 |
17 | const SelectedInfoTab = ({ selectedTab, ...data }: Props) => {
18 | if (selectedTab === 0) {
19 | return ;
20 | }
21 |
22 | if (selectedTab === 1) {
23 | return ;
24 | }
25 |
26 | return null;
27 | };
28 |
29 | export default SelectedInfoTab;
30 |
--------------------------------------------------------------------------------
/src/components/info/topbar/addToListButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Dialog } from "@/components/ui/dialog";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuTrigger,
6 | } from "@/components/ui/dropdown-menu";
7 | import ListsDropdownContent from "@/features/lists/components/dropdownContent";
8 | import NewListDialogContent from "@/features/lists/components/newListDialogContent";
9 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
10 | import { Bookmark } from "lucide-react";
11 | import { useState } from "react";
12 |
13 | export const AddToListButton = (props: IGDBReturnDataType) => {
14 | const [openDialog, setOpenDialog] = useState(false);
15 |
16 | return (
17 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/info/topbar/backButton.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronLeft } from "lucide-react";
2 |
3 | interface BackButtonProps {
4 | onClick: () => void;
5 | }
6 |
7 | export const BackButton = ({ onClick }: BackButtonProps) => (
8 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/info/topbar/index.tsx:
--------------------------------------------------------------------------------
1 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
2 | import { AddToListButton } from "./addToListButton";
3 | import { BackButton } from "./backButton";
4 | import { Title } from "./title";
5 |
6 | interface TopbarProps {
7 | data: IGDBReturnDataType;
8 | onBack: () => void;
9 | titleText: string;
10 | }
11 |
12 | export const Topbar = ({ onBack, titleText, data }: TopbarProps) => (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/components/info/topbar/title.tsx:
--------------------------------------------------------------------------------
1 | import { H3 } from "@/components/ui/typography";
2 |
3 | interface TitleProps {
4 | text: string;
5 | }
6 |
7 | export const Title = ({ text }: TitleProps) => {text}
;
8 |
--------------------------------------------------------------------------------
/src/components/protonDbBadge/index.tsx:
--------------------------------------------------------------------------------
1 | import { ProtonDBTierColor } from "@/@types";
2 | import { useProtonDb } from "@/hooks";
3 |
4 | import protonDBBadge from "@/assets/protondb.png";
5 |
6 | interface Props {
7 | appId: string;
8 | }
9 |
10 | const ProtonDbBadge = ({ appId }: Props) => {
11 | const { data, error, isPending } = useProtonDb(appId);
12 |
13 | if (isPending || error) return null;
14 | if (!data) return null;
15 |
16 | const { tier } = data;
17 |
18 | if (tier === "pending" || !tier) return null;
19 |
20 | const color = ProtonDBTierColor[tier as keyof typeof ProtonDBTierColor];
21 |
22 | return (
23 |
27 |

28 | {/*
{tier} */}
29 |
30 | );
31 | };
32 |
33 | export default ProtonDbBadge;
34 |
--------------------------------------------------------------------------------
/src/components/skeletons/banner.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const BannerSkeleton = () => {
4 | return ;
5 | };
6 |
7 | export default BannerSkeleton;
8 |
--------------------------------------------------------------------------------
/src/components/skeletons/defaultCard.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const DefaultCardSkeleton = () => {
4 | return (
5 |
6 | );
7 | };
8 |
9 | export default DefaultCardSkeleton;
10 |
--------------------------------------------------------------------------------
/src/components/skeletons/genericRow.tsx:
--------------------------------------------------------------------------------
1 | import DefaultCardSkeleton from "@/components/skeletons/defaultCard";
2 |
3 | const GenericRowSkeleton = () => {
4 | return (
5 |
6 | {[...Array(6)].map((_, index) => (
7 |
8 | ))}
9 |
10 | );
11 | };
12 |
13 | export default GenericRowSkeleton;
14 |
--------------------------------------------------------------------------------
/src/components/skeletons/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./banner";
2 | export * from "./defaultCard";
3 | export * from "./genericRow";
4 | export * from "./info";
5 |
--------------------------------------------------------------------------------
/src/components/skeletons/info/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./top.skeleton";
2 | export * from "./topbar.skeleton";
3 |
--------------------------------------------------------------------------------
/src/components/skeletons/info/topbar.skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | const TopbarSkeleton = () => {
4 | return (
5 |
6 | {/* BACK BUTTON AND TITLE */}
7 |
8 | {/* Back button */}
9 | {/* Title */}
10 |
11 |
12 | {/* ADD TO LIST BUTTON */}
13 |
14 |
15 | );
16 | };
17 |
18 | export default TopbarSkeleton;
19 |
--------------------------------------------------------------------------------
/src/components/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "lucide-react";
2 |
3 | interface SpinnerProps {
4 | size?: number; // Allow customization of the size
5 | className?: string; // Additional custom styling
6 | }
7 |
8 | const Spinner = ({ size = 24, className = "" }: SpinnerProps) => {
9 | return (
10 |
15 | );
16 | };
17 |
18 | export default Spinner;
19 |
--------------------------------------------------------------------------------
/src/components/starts.tsx:
--------------------------------------------------------------------------------
1 | import { Star, StarHalf } from "lucide-react";
2 |
3 | interface StarsProps {
4 | stars: number; // Rating out of 10
5 | }
6 |
7 | const Stars = ({ stars }: StarsProps) => {
8 | if (stars === 0) return null;
9 |
10 | const actual_stars = stars / 2; // Convert to a 5-star scale
11 | const fullStars = Math.floor(actual_stars); // Full stars
12 | const hasHalfStar = actual_stars % 1 >= 0.5; // Half star if fractional part >= 0.5
13 |
14 | return (
15 |
16 | {/* Render full stars */}
17 | {Array.from({ length: fullStars }, (_, i) => (
18 |
22 | ))}
23 |
24 | {/* Render half star if applicable */}
25 | {hasHalfStar && (
26 |
27 |
28 |
29 |
30 | )}
31 |
32 | );
33 | };
34 |
35 | export default Stars;
36 |
--------------------------------------------------------------------------------
/src/components/titleBar/control.tsx:
--------------------------------------------------------------------------------
1 | import { cn, invoke } from "@/lib";
2 | import { HtmlHTMLAttributes } from "react";
3 |
4 | interface TitleBarControlProps extends HtmlHTMLAttributes {
5 | type: "minimize" | "maximize" | "close";
6 | }
7 |
8 | const TitleBarControl = ({
9 | type,
10 | className,
11 | ...props
12 | }: TitleBarControlProps) => {
13 | return (
14 |
23 | );
24 | };
25 |
26 | export default TitleBarControl;
27 |
--------------------------------------------------------------------------------
/src/components/titleBar/controlWithIcon.tsx:
--------------------------------------------------------------------------------
1 | import { invoke } from "@/lib";
2 | import { HtmlHTMLAttributes, PropsWithChildren } from "react";
3 |
4 | interface TitleBarControlProps extends HtmlHTMLAttributes {
5 | type: "minimize" | "maximize" | "close";
6 | }
7 |
8 | const TitleBarControlWithIcon = ({
9 | type,
10 | className,
11 | children,
12 | ...props
13 | }: PropsWithChildren) => {
14 | return (
15 |
23 | );
24 | };
25 |
26 | export default TitleBarControlWithIcon;
27 |
--------------------------------------------------------------------------------
/src/components/titleBar/icons.tsx:
--------------------------------------------------------------------------------
1 | import { Maximize2, Minus, X } from "lucide-react";
2 | import TitleBarControlWithIcon from "./controlWithIcon";
3 |
4 | const TitleBarIcons = () => {
5 | return (
6 | <>
7 |
8 |
12 |
13 |
14 |
18 |
19 |
20 |
24 |
25 | >
26 | );
27 | };
28 |
29 | export default TitleBarIcons;
30 |
--------------------------------------------------------------------------------
/src/components/titleBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { useLanguageContext } from "@/contexts/I18N";
2 | import { useSettings } from "@/hooks";
3 | import TitleBarIcons from "./icons";
4 | import TitleBarTrafficLights from "./traffic-lights";
5 | import { H4 } from "../ui/typography";
6 |
7 | const TitleBar = () => {
8 | const { t } = useLanguageContext();
9 | const { settings } = useSettings();
10 |
11 | const titleBarStyle = settings?.titleBarStyle;
12 |
13 | if (titleBarStyle === "none") return null;
14 | if (titleBarStyle === "native") return null;
15 |
16 | return (
17 |
18 |
19 | {/* Title */}
20 |
21 |
{t("falkor")}
22 |
23 |
24 | {titleBarStyle === "icons" ? (
25 |
26 | ) : (
27 |
28 | )}
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default TitleBar;
36 |
--------------------------------------------------------------------------------
/src/components/titleBar/traffic-lights.tsx:
--------------------------------------------------------------------------------
1 | import TitleBarControl from "./control";
2 |
3 | const TitleBarTrafficLights = () => {
4 | return (
5 | <>
6 |
10 |
14 |
18 | >
19 | );
20 | };
21 |
22 | export default TitleBarTrafficLights;
23 |
--------------------------------------------------------------------------------
/src/components/trailer/dialogContent.tsx:
--------------------------------------------------------------------------------
1 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
2 | import MediaTrailer from "../info/media/trailer";
3 | import { DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
4 |
5 | type Props = Pick;
6 |
7 | const TrailerDialogContent = ({ name, videos }: Props) => {
8 | return (
9 |
10 |
11 | {name}
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default TrailerDialogContent;
21 |
--------------------------------------------------------------------------------
/src/components/trailer/index.tsx:
--------------------------------------------------------------------------------
1 | import { useLanguageContext } from "@/contexts/I18N";
2 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
3 | import { Button } from "../ui/button";
4 | import { Dialog, DialogTrigger } from "../ui/dialog";
5 | import TrailerDialogContent from "./dialogContent";
6 |
7 | type Props = Pick;
8 |
9 | const TrailerButton = (props: Props) => {
10 | const { t } = useLanguageContext();
11 |
12 | return (
13 |
20 | );
21 | };
22 |
23 | export default TrailerButton;
24 |
--------------------------------------------------------------------------------
/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2 |
3 | const AspectRatio = AspectRatioPrimitive.Root
4 |
5 | export { AspectRatio }
6 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from "class-variance-authority";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-background text-secondary-foreground hover:bg-background/50",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { CheckIcon } from "@radix-ui/react-icons"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverAnchor = PopoverPrimitive.Anchor
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
32 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SliderPrimitive from "@radix-ui/react-slider"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ))
24 | Slider.displayName = SliderPrimitive.Root.displayName
25 |
26 | export { Slider }
27 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes"
2 | import { Toaster as Sonner } from "sonner"
3 |
4 | type ToasterProps = React.ComponentProps
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitives from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ))
25 | Switch.displayName = SwitchPrimitives.Root.displayName
26 |
27 | export { Switch }
28 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider;
7 |
8 | const Tooltip = TooltipPrimitive.Root;
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger;
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ));
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
27 |
28 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
29 |
--------------------------------------------------------------------------------
/src/features/accounts/components/table/index.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalAccount } from "@/@types/accounts";
2 | import { invoke } from "@/lib";
3 | import { useQuery } from "@tanstack/react-query";
4 | import { columns } from "./columns";
5 | import { DataTable } from "./data-table";
6 |
7 | const AccountsTable = () => {
8 | const { data, isPending, error, isError } = useQuery({
9 | queryKey: ["accounts", "all"],
10 | queryFn: async () => {
11 | const data = invoke, string | undefined>(
12 | "external-accounts:get-all"
13 | );
14 |
15 | return data;
16 | },
17 | });
18 |
19 | if (isPending) {
20 | return Loading...
;
21 | }
22 |
23 | if (isError) {
24 | return Error: {error.message}
;
25 | }
26 |
27 | if (!data) {
28 | return No data
;
29 | }
30 |
31 | return (
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default AccountsTable;
39 |
--------------------------------------------------------------------------------
/src/features/achievements/components/cards/achievement.tsx:
--------------------------------------------------------------------------------
1 | import { ISchemaForGameAchievement } from "@/@types";
2 | import { TypographySmall, TypographyMuted } from "@/components/ui/typography";
3 | import { cn } from "@/lib";
4 |
5 | interface Props extends ISchemaForGameAchievement {
6 | unlocked: boolean;
7 | }
8 |
9 | export const AchievementCard = ({
10 | displayName,
11 |
12 | icongray,
13 | icon,
14 | description,
15 | unlocked,
16 | }: Props) => {
17 | return (
18 |
19 |

24 |
25 |
26 |
31 | {displayName}
32 |
33 | {description && (
34 |
35 | {description}
36 |
37 | )}
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/features/achievements/hooks/useGetAchievementsData.ts:
--------------------------------------------------------------------------------
1 | import { ISchemaForGame, ISchemaForGameAchievement } from "@/@types";
2 | import { useSettings } from "@/hooks";
3 | import { useQuery } from "@tanstack/react-query";
4 |
5 | interface Achievement {
6 | steamId: string;
7 | }
8 |
9 | export const useGetAchievementsData = ({ steamId }: Achievement) => {
10 | const { settings } = useSettings();
11 |
12 | const fetcher = async (): Promise> => {
13 | const apiURL = settings.api_base_url;
14 | const response = await fetch(`${apiURL}/achievements/${steamId}`);
15 |
16 | if (!response.ok) return [];
17 |
18 | const data: ISchemaForGame = await response.json();
19 |
20 | if (!data?.game?.availableGameStats?.achievements) return [];
21 | return data.game.availableGameStats.achievements;
22 | };
23 |
24 | return useQuery({
25 | queryKey: ["achievements", steamId],
26 | queryFn: fetcher,
27 | refetchOnMount: false,
28 | refetchOnReconnect: false,
29 | refetchOnWindowFocus: false,
30 | enabled: !!steamId,
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/src/features/achievements/hooks/useGetUnlockedAchievements.ts:
--------------------------------------------------------------------------------
1 | import { AchievementDBItem } from "@/@types";
2 | import { invoke } from "@/lib";
3 | import { useQuery } from "@tanstack/react-query";
4 |
5 | export const useGetUnlockedAchievements = (gameId: string) => {
6 | const fetcher = async (): Promise => {
7 | const unlocked = await invoke(
8 | "achievements:get-unlocked",
9 | gameId
10 | );
11 | return unlocked ?? [];
12 | };
13 |
14 | return useQuery({
15 | queryKey: ["achievements", "unlocked", gameId],
16 | queryFn: fetcher,
17 | refetchOnMount: false,
18 | refetchOnReconnect: false,
19 | refetchOnWindowFocus: false,
20 | enabled: !!gameId,
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/src/features/downloads/components/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 | import { ArrowDownToLine } from "lucide-react";
3 |
4 | export function EmptyState() {
5 | return (
6 |
7 |
8 |
9 | No Downloads Yet
10 |
11 | Downloads you start will appear here.
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/features/downloads/components/ErrorState.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3 |
4 | interface ErrorStateProps {
5 | error: string;
6 | retry: () => void;
7 | }
8 |
9 | export function ErrorState({ error, retry }: ErrorStateProps) {
10 | return (
11 |
12 |
13 |
14 | Error Loading Downloads
15 |
16 |
17 |
18 | {error}
19 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/features/downloads/components/LoadingState.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 |
3 | export function LoadingState() {
4 | return (
5 |
6 |
7 | Loading downloads...
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/features/downloads/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./DownloadItem";
2 | export * from "./DownloadTabs";
3 | export * from "./EmptyState";
4 | export * from "./ErrorState";
5 | export * from "./LoadingState";
6 |
--------------------------------------------------------------------------------
/src/features/downloads/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useDownloadActions";
2 | export * from "./useDownloadEvents";
3 | export * from "./useDownloadStats";
4 |
--------------------------------------------------------------------------------
/src/features/downloads/hooks/useDownloadActions.ts:
--------------------------------------------------------------------------------
1 | import { useDownloadsStore } from "@/stores/downloads";
2 |
3 | /**
4 | * Hook to manage download actions
5 | * Provides functions to add, pause, resume, cancel, and remove downloads
6 | */
7 | export function useDownloadActions() {
8 | const {
9 | addDownload,
10 | pauseDownload,
11 | resumeDownload,
12 | cancelDownload,
13 | removeDownload,
14 | clearCompletedDownloads,
15 | setPriority,
16 | throttleDownload,
17 | throttleUpload,
18 | } = useDownloadsStore();
19 |
20 | return {
21 | addDownload,
22 | pauseDownload,
23 | resumeDownload,
24 | cancelDownload,
25 | removeDownload,
26 | clearCompletedDownloads,
27 | setPriority,
28 | throttleDownload,
29 | throttleUpload,
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/features/downloads/hooks/useDownloadStats.ts:
--------------------------------------------------------------------------------
1 | import { DownloadStatus } from "@/@types";
2 | import { useDownloadsStore } from "@/stores/downloads";
3 |
4 | /**
5 | * Hook to get download statistics
6 | * Returns counts of downloads by status
7 | */
8 | export function useDownloadStats() {
9 | const { downloads } = useDownloadsStore();
10 |
11 | const activeCount = downloads.filter(
12 | (d) => d.status === DownloadStatus.DOWNLOADING
13 | ).length;
14 | const queuedCount = downloads.filter(
15 | (d) => d.status === DownloadStatus.QUEUED
16 | ).length;
17 | const pausedCount = downloads.filter(
18 | (d) => d.status === DownloadStatus.PAUSED
19 | ).length;
20 | const completedCount = downloads.filter(
21 | (d) => d.status === DownloadStatus.COMPLETED
22 | ).length;
23 | const failedCount = downloads.filter(
24 | (d) => d.status === DownloadStatus.FAILED
25 | ).length;
26 |
27 | const totalCount = downloads.length;
28 | const hasDownloads = totalCount > 0;
29 | const hasActiveDownloads = activeCount > 0;
30 |
31 | return {
32 | activeCount,
33 | queuedCount,
34 | pausedCount,
35 | completedCount,
36 | failedCount,
37 | totalCount,
38 | hasDownloads,
39 | hasActiveDownloads,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/features/home/components/GameRows.tsx:
--------------------------------------------------------------------------------
1 | import RowContainer from "@/components/containers/row";
2 | import { useLanguageContext } from "@/contexts/I18N";
3 |
4 | const GameRows = () => {
5 | const { t } = useLanguageContext();
6 |
7 | return (
8 |
9 |
15 |
16 |
22 |
23 | );
24 | };
25 |
26 | export default GameRows;
--------------------------------------------------------------------------------
/src/features/home/components/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import { buttonVariants } from "@/components/ui/button";
2 | import { H1, TypographyMuted } from "@/components/ui/typography";
3 | import { useLanguageContext } from "@/contexts/I18N";
4 | import { Link } from "@tanstack/react-router";
5 |
6 | const HeroSection = () => {
7 | const { t } = useLanguageContext();
8 |
9 | return (
10 |
11 |
12 |
13 |
14 | {t("falkor")}
15 | {t("logo_hover")}
16 |
17 |
18 | Discover, play, and manage your favorite games in one place
19 |
20 |
21 |
27 | Browse Library
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default HeroSection;
36 |
--------------------------------------------------------------------------------
/src/features/library/components/activeLibrary.tsx:
--------------------------------------------------------------------------------
1 | import ActiveLibraryGame from "./containers/activeLibraryGame";
2 | import ActiveLibraryList from "./containers/activeLibraryList";
3 | import LibraryHeader from "./header";
4 |
5 | type ActiveLibraryProps =
6 | | {
7 | title: string;
8 | type: "game";
9 | }
10 | | {
11 | title: string;
12 | description?: string;
13 | type: "list";
14 | listId: number;
15 | };
16 |
17 | const ActiveLibrary = (props: ActiveLibraryProps) => {
18 | const { type } = props;
19 |
20 | return (
21 |
22 | {/* Header */}
23 |
24 |
25 | {/* Content */}
26 |
27 | {type === "game" ? (
28 |
29 | ) : type === "list" ? (
30 |
31 | ) : (
32 |
Unknown type
33 | )}
34 |
35 |
36 | );
37 | };
38 |
39 | export default ActiveLibrary;
40 |
--------------------------------------------------------------------------------
/src/features/library/components/cards/continuePlaying/BackgroundImage.tsx:
--------------------------------------------------------------------------------
1 | interface BackgroundImageProps {
2 | bgImage: string;
3 | className?: string;
4 | }
5 |
6 | const BackgroundImage: React.FC = ({
7 | bgImage,
8 | className,
9 | }) => {
10 | const isRemoteImage = /^https?:\/\//i.test(bgImage);
11 | const realImagePath = isRemoteImage ? bgImage : `local:${encodeURI(bgImage)}`;
12 |
13 | return (
14 |
22 | );
23 | };
24 |
25 | export default BackgroundImage;
26 |
--------------------------------------------------------------------------------
/src/features/library/components/cards/continuePlaying/ContinuePlayingCardOverlay/actions/delete.tsx:
--------------------------------------------------------------------------------
1 | import Confirmation from "@/components/confirmation";
2 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
3 | import { useLanguageContext } from "@/contexts/I18N";
4 |
5 | interface DeleteDialogProps {
6 | deleteGame: () => void;
7 | fetchGames: () => void;
8 | }
9 |
10 | const DeleteDialog = ({ deleteGame, fetchGames }: DeleteDialogProps) => {
11 | const { t } = useLanguageContext();
12 |
13 | return (
14 | {
16 | await deleteGame();
17 | await fetchGames();
18 | }}
19 | >
20 | e.preventDefault()}>
21 | {t("delete")}
22 |
23 |
24 | );
25 | };
26 |
27 | export default DeleteDialog;
28 |
--------------------------------------------------------------------------------
/src/features/library/components/cards/continuePlaying/PlayStopButton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib";
2 | import { Play } from "lucide-react";
3 | import { MdStop } from "react-icons/md";
4 |
5 | interface PlayStopButtonProps {
6 | isPlaying: boolean;
7 | playGame: () => void;
8 | stopGame: () => void;
9 | }
10 |
11 | const PlayStopButton: React.FC = ({
12 | isPlaying,
13 | playGame,
14 | stopGame,
15 | }) => (
16 |
34 | );
35 |
36 | export default PlayStopButton;
37 |
--------------------------------------------------------------------------------
/src/features/library/components/cards/newGame.tsx:
--------------------------------------------------------------------------------
1 | import { PlusIcon } from "lucide-react";
2 |
3 | const NewGameCard = () => {
4 | return (
5 |
6 | {/* BG */}
7 |
8 |
11 |
12 | );
13 | };
14 |
15 | export { NewGameCard };
16 |
--------------------------------------------------------------------------------
/src/features/library/components/containers/activeLibraryGame.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useGames } from "../../hooks/useGames";
3 | import ContinuePlayingCard from "../cards/continuePlaying";
4 | import { H5 } from "@/components/ui/typography";
5 |
6 | const ActiveLibraryGame = () => {
7 | const { fetchGames, deleteGame, updateGame, games } = useGames(true);
8 |
9 | const gamesCount = useMemo(() => Object.values(games)?.length, [games]);
10 |
11 | return gamesCount ? (
12 |
13 | {Object.values(games).map((game) => (
14 |
22 | ))}
23 |
24 | ) : (
25 | You have not added any games to continue playing.
26 | );
27 | };
28 |
29 | export default ActiveLibraryGame;
30 |
--------------------------------------------------------------------------------
/src/features/library/components/containers/activeLibraryList.tsx:
--------------------------------------------------------------------------------
1 | import ListCard from "@/components/cards/listCard";
2 | import { H5, P } from "@/components/ui/typography";
3 | import { useLists } from "@/features/lists/hooks/useLists";
4 | import { useQuery } from "@tanstack/react-query";
5 | import { useMemo } from "react";
6 |
7 | interface ActiveLibraryListProps {
8 | listId: number;
9 | }
10 |
11 | const ActiveLibraryList = ({ listId }: ActiveLibraryListProps) => {
12 | const { fetchGamesInList } = useLists();
13 |
14 | const { data, isPending, isError } = useQuery({
15 | queryKey: ["library", listId],
16 | queryFn: async () => {
17 | return await fetchGamesInList(listId);
18 | },
19 | });
20 |
21 | const listCount = useMemo(() => data?.length, [data]);
22 |
23 | if (isPending) {
24 | return (
25 |
26 |
Loading...
27 |
28 | );
29 | }
30 |
31 | if (isError) {
32 | return (
33 |
34 |
Something went wrong. Please try again later.
35 |
36 | );
37 | }
38 |
39 | return listCount ? (
40 |
41 | {data.map((game) => (
42 |
43 | ))}
44 |
45 | ) : (
46 | No games in this list.
47 | );
48 | };
49 |
50 | export default ActiveLibraryList;
51 |
--------------------------------------------------------------------------------
/src/features/library/components/continuePlaying.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogTrigger } from "@/components/ui/dialog";
2 | import { useMemo } from "react";
3 | import { useGames } from "../hooks/useGames";
4 | import { NewGameCard } from "./cards/newGame";
5 | import GamesContainer from "./containers/games";
6 | import NewGameModal from "./modals/newGame";
7 |
8 | const ContinuePlaying = () => {
9 | const { games, fetchGames, deleteGame, updateGame } = useGames();
10 |
11 | const games_count = useMemo(() => Object.values(games)?.length, [games]);
12 |
13 | if (games_count)
14 | return (
15 |
21 | );
22 |
23 | return (
24 |
31 | );
32 | };
33 |
34 | export default ContinuePlaying;
35 |
--------------------------------------------------------------------------------
/src/features/library/components/gameFormInput.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormControl,
3 | FormItem,
4 | FormLabel,
5 | FormMessage,
6 | } from "@/components/ui/form";
7 | import { Input } from "@/components/ui/input";
8 | import { cn } from "@/lib";
9 | import { InputHTMLAttributes, JSX } from "react";
10 | import { ControllerRenderProps, FieldValues } from "react-hook-form";
11 |
12 | interface GameFormInputProps
13 | extends InputHTMLAttributes {
14 | text: string;
15 | description: string;
16 | required?: boolean;
17 | field: ControllerRenderProps;
18 | Button?: JSX.Element;
19 | }
20 |
21 | const GameFormInput = ({
22 | description,
23 | required,
24 | field,
25 | Button,
26 | className,
27 | text,
28 | ...props
29 | }: GameFormInputProps) => {
30 | return (
31 |
32 |
33 | {text}
34 | {required ? "*" : null}
35 |
36 |
37 |
38 |
50 |
51 | {Button}
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default GameFormInput;
59 |
--------------------------------------------------------------------------------
/src/features/library/components/header/index.tsx:
--------------------------------------------------------------------------------
1 | import { Book, Play } from "lucide-react";
2 | import LibraryListActions from "./listActions";
3 | import { H2, P } from "@/components/ui/typography";
4 |
5 | interface GameProps {
6 | type: "game";
7 | title: string;
8 | }
9 |
10 | interface ListProps {
11 | type: "list";
12 | listId: number;
13 | title: string;
14 | description?: string;
15 | }
16 |
17 | type Props = GameProps | ListProps;
18 |
19 | const LibraryHeader = (props: Props) => {
20 | const { type, title } = props;
21 |
22 | return (
23 |
24 |
25 | {type === "game" ? (
26 |
27 | ) : (
28 |
29 | )}
30 |
{title}
31 | {type === "list" && (
32 |
33 | )}
34 |
35 | {type === "list" && props.description && (
36 |
{props.description}
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default LibraryHeader;
43 |
--------------------------------------------------------------------------------
/src/features/library/components/mediaCarousel/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { MainMediaDisplay } from "./mainMediaDisplay";
3 | import { ThumbnailCarousel } from "./thumbnailCarousel";
4 | import { Thumbnail } from "./types";
5 |
6 | interface MediaCarouselProps {
7 | mainMedia: Thumbnail;
8 | thumbnails: Array | undefined;
9 | }
10 |
11 | export const MediaCarousel = ({
12 | mainMedia,
13 | thumbnails,
14 | }: MediaCarouselProps) => {
15 | const [selectedMedia, setSelectedMedia] = useState(mainMedia);
16 |
17 | return (
18 |
19 | {/* Main Media Display */}
20 |
21 |
22 | {/* Thumbnail Carousel */}
23 | setSelectedMedia(media)}
26 | />
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/features/library/components/mediaCarousel/mainMediaDisplay.tsx:
--------------------------------------------------------------------------------
1 | import IGDBImage from "@/components/IGDBImage";
2 | import { AspectRatio } from "@/components/ui/aspect-ratio";
3 | import { Thumbnail } from "./types";
4 |
5 | interface MainMediaDisplayProps {
6 | media: Thumbnail;
7 | }
8 |
9 | export const MainMediaDisplay = ({ media }: MainMediaDisplayProps) => (
10 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/features/library/components/mediaCarousel/thumbnailCarousel.tsx:
--------------------------------------------------------------------------------
1 | import IGDBImage from "@/components/IGDBImage";
2 | import { AspectRatio } from "@/components/ui/aspect-ratio";
3 | import {
4 | Carousel,
5 | CarouselContent,
6 | CarouselItem,
7 | } from "@/components/ui/carousel";
8 | import { Thumbnail } from "./types";
9 |
10 | interface ThumbnailCarouselProps {
11 | thumbnails: Thumbnail[] | undefined;
12 | onThumbnailClick: (media: Thumbnail) => void;
13 | }
14 |
15 | export const ThumbnailCarousel = ({
16 | thumbnails,
17 | onThumbnailClick,
18 | }: ThumbnailCarouselProps) => {
19 | if (!thumbnails) return null;
20 |
21 | return (
22 |
23 |
24 | {thumbnails.map((thumb, index) => (
25 |
26 |
27 |
onThumbnailClick(thumb)}
31 | >
32 |
37 |
38 |
39 |
40 | ))}
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/features/library/components/mediaCarousel/types.ts:
--------------------------------------------------------------------------------
1 | import { IGDBReturnDataTypeCover } from "@/lib/api/igdb/types";
2 |
3 | export interface Thumbnail {
4 | media: IGDBReturnDataTypeCover;
5 | alt: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/library/components/modals/newGame/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const newGameFormSchema = z.object({
4 | gameName: z.string().min(1, { message: "Required" }),
5 | gamePath: z.string().min(1, { message: "Required" }),
6 | gameId: z.string().min(1, { message: "Required" }),
7 | gameIcon: z.string().min(1, { message: "Required" }),
8 | gameArgs: z.string().optional(),
9 | gameCommand: z
10 | .string()
11 | .optional()
12 | .refine((s) => !s?.includes(" "), "No Spaces!"),
13 | igdbId: z.string().optional(),
14 | steamId: z.string().optional(),
15 | winePrefixFolder: z.string().optional(),
16 | });
17 |
18 | export type NewGameFormSchema = z.infer;
19 |
--------------------------------------------------------------------------------
/src/features/library/components/playtime.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { P } from "@/components/ui/typography";
3 | import { cn } from "@/lib/utils";
4 | import { Clock } from "lucide-react";
5 | import ms from "ms";
6 |
7 | interface PlaytimeProps {
8 | playtime: number;
9 | className?: string;
10 | }
11 |
12 | const Playtime = ({ playtime, className }: PlaytimeProps) => {
13 | if (!playtime) return ;
14 |
15 | return (
16 |
22 |
23 |
24 | {ms(playtime, {
25 | long: true,
26 | })}
27 |
28 |
29 | );
30 | };
31 |
32 | export default Playtime;
33 |
--------------------------------------------------------------------------------
/src/features/library/hooks/useGames.ts:
--------------------------------------------------------------------------------
1 | import { useGamesStore } from "@/stores/games";
2 | import { useEffect } from "react";
3 |
4 | export const useGames = (fetch: boolean = true) => {
5 | const {
6 | games,
7 | loading,
8 | error,
9 | addGame,
10 | getGameById,
11 | updateGame,
12 | deleteGame,
13 | fetchGames,
14 | getGameByIGDBId,
15 | } = useGamesStore();
16 |
17 | useEffect(() => {
18 | if (fetch) fetchGames();
19 | }, [fetch, fetchGames]);
20 |
21 | return {
22 | fetchGames,
23 | games,
24 | loading,
25 | error,
26 | addGame,
27 | getGameById,
28 | updateGame,
29 | deleteGame,
30 | getGameByIGDBId,
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/src/features/lists/components/container/listsContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useLists } from "../../hooks/useLists";
2 | import ListContainer from "./listContainer";
3 |
4 | const ListsContainer = () => {
5 | const { lists } = useLists();
6 |
7 | if (!lists) return null;
8 |
9 | return lists.map((list) => (
10 |
11 | ));
12 | };
13 |
14 | export default ListsContainer;
15 |
--------------------------------------------------------------------------------
/src/features/lists/components/listsDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Dialog } from "@/components/ui/dialog";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuTrigger,
6 | } from "@/components/ui/dropdown-menu";
7 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
8 | import { List } from "lucide-react";
9 | import { useState } from "react";
10 | import ListsDropdownContent from "./dropdownContent";
11 | import NewListDialogContent from "./newListDialogContent";
12 |
13 | const ListsDropdown = (props: IGDBReturnDataType) => {
14 | const [openDialog, setOpenDialog] = useState(false);
15 |
16 | return (
17 |
30 | );
31 | };
32 |
33 | export default ListsDropdown;
34 |
--------------------------------------------------------------------------------
/src/features/lists/hooks/useLists.ts:
--------------------------------------------------------------------------------
1 | import { useListsStore } from "@/stores/lists";
2 | import { useEffect } from "react";
3 |
4 | export const useLists = () => {
5 | const store = useListsStore();
6 |
7 | useEffect(() => {
8 | if (store.hasDoneFirstFetch) return;
9 |
10 | store.fetchLists();
11 | store.setHasDoneFirstFetch();
12 | }, [store]);
13 |
14 | return {
15 | lists: store.lists,
16 | gamesInList: store.gamesInList,
17 | loading: store.loading,
18 | error: store.error,
19 | removeGameFromList: store.removeGameFromList,
20 | fetchLists: store.fetchLists,
21 | createList: store.createList,
22 | addGameToList: store.addGameToList,
23 | fetchGamesInList: store.fetchGamesInList,
24 | deleteList: store.deleteList,
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/src/features/navigation/components/containers/bottom.tsx:
--------------------------------------------------------------------------------
1 | import { useLanguageContext } from "@/contexts/I18N";
2 | import { DownloadIcon, Settings2 } from "lucide-react";
3 | import NavItem from "../item";
4 |
5 | const NavBarBottom = () => {
6 | const { t } = useLanguageContext();
7 |
8 | return (
9 |
10 | }
14 | />
15 | }
19 | />
20 |
21 | );
22 | };
23 |
24 | export default NavBarBottom;
25 |
--------------------------------------------------------------------------------
/src/features/navigation/components/containers/middle.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Carousel,
3 | CarouselContent,
4 | CarouselItem,
5 | } from "@/components/ui/carousel";
6 | import { useGames } from "@/features/library/hooks/useGames";
7 | import { useMemo } from "react";
8 | import NavBarContinuePlayingCard from "../cards/playing";
9 |
10 | export const NavBarMiddle = () => {
11 | const { games } = useGames(true);
12 |
13 | const gamesToShow = useMemo(() => {
14 | const gamesArray = Object.values(games);
15 | if (!gamesArray?.length) return [];
16 | gamesArray.sort(
17 | (a, b) => Number(b.game_last_played) - Number(a.game_last_played)
18 | );
19 | return gamesArray.slice(0, 5);
20 | }, [games]);
21 |
22 | if (!Object.values(games)?.length) return null;
23 |
24 | return (
25 |
26 |
35 |
36 | {...gamesToShow.map((game) => {
37 | return (
38 |
39 |
40 |
41 | );
42 | })}
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/features/navigation/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { useSettings } from "@/hooks";
2 | import { cn, shouldHideTitleBar } from "@/lib";
3 | import NavBarBottom from "./containers/bottom";
4 | import { NavBarMiddle } from "./containers/middle";
5 | import NavBarTop from "./containers/top";
6 |
7 | const NavBar = () => {
8 | const { settings } = useSettings();
9 |
10 | const titleBarStyle = settings?.titleBarStyle;
11 |
12 | return (
13 |
29 | );
30 | };
31 |
32 | export default NavBar;
33 |
--------------------------------------------------------------------------------
/src/features/plugins/providers/hooks/useProviders.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { providersApi } from "../utils/api/providersApi";
3 |
4 | export const useProviders = (params?: {
5 | limit?: number;
6 | offset?: number;
7 | search?: string;
8 | }) => {
9 | const { data, isLoading, error, isError, refetch } = useQuery({
10 | queryKey: ["providers", params],
11 | queryFn: () => providersApi.getProviders(params),
12 | });
13 |
14 | return {
15 | providers: data,
16 | isLoading: isLoading,
17 | error: error,
18 | isError: isError,
19 | refetch: refetch,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/features/plugins/providers/utils/api/providersApi.ts:
--------------------------------------------------------------------------------
1 | import {
2 | APIResponse,
3 | PluginProvider,
4 | PluginSetupJSON,
5 | } from "@team-falkor/shared-types";
6 |
7 | type GetProvidersParams = {
8 | limit?: number;
9 | offset?: number;
10 | search?: string;
11 | };
12 |
13 | export const providersApi = {
14 | getProviders: async (
15 | params?: GetProvidersParams
16 | ): Promise>> => {
17 | const searchParams = new URLSearchParams();
18 | if (params?.limit) searchParams.append("limit", params.limit.toString());
19 | if (params?.offset) searchParams.append("offset", params.offset.toString());
20 | if (params?.search) searchParams.append("search", params.search);
21 |
22 | const response = await fetch(
23 | `https://api.falkor.moe/providers?${searchParams.toString()}`
24 | );
25 | return await response.json();
26 | },
27 |
28 | addProvider: async (setupData: {
29 | setupJSON?: PluginSetupJSON;
30 | setupUrl?: string;
31 | }): Promise> => {
32 | const response = await fetch(`https://api.falkor.moe/providers`, {
33 | method: "PUT",
34 | headers: {
35 | "Content-Type": "application/json",
36 | },
37 | body: JSON.stringify(setupData),
38 | });
39 | const data: APIResponse = await response.json();
40 |
41 | return data;
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/features/realDebrid/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import { getRealDebridAuthInstance } from "@/lib/api/realdebrid/auth";
2 |
3 | const realDebridAuth = getRealDebridAuthInstance();
4 |
5 | export const obtainDeviceCode = async () => {
6 | return await realDebridAuth.obtainDeviceCode();
7 | };
8 |
9 | export const pollForCredentials = async (
10 | deviceCode: string,
11 | interval: number,
12 | expiresIn: number
13 | ) => {
14 | return await realDebridAuth.pollForCredentials(
15 | deviceCode,
16 | interval,
17 | expiresIn
18 | );
19 | };
20 |
21 | export const obtainAccessToken = async (deviceCode: string) => {
22 | return await realDebridAuth.obtainAccessToken(deviceCode);
23 | };
24 |
--------------------------------------------------------------------------------
/src/features/search/components/card.tsx:
--------------------------------------------------------------------------------
1 | import { P, TypographyMuted } from "@/components/ui/typography";
2 | import { IGDBReturnDataType } from "@/lib/api/igdb/types";
3 | import { Link } from "@tanstack/react-router";
4 |
5 | const SearchCard = ({
6 | name,
7 | id,
8 | setOpen,
9 | release_dates,
10 | }: IGDBReturnDataType & {
11 | setOpen: React.Dispatch>;
12 | }) => {
13 | const year = release_dates?.[0]?.human;
14 |
15 | return (
16 | setOpen(false)}
22 | >
23 |
24 |
{name}
25 |
{year}
26 |
27 |
28 | );
29 | };
30 |
31 | export default SearchCard;
32 |
--------------------------------------------------------------------------------
/src/features/settings/components/linkGroup.tsx:
--------------------------------------------------------------------------------
1 | import { LinkItemType } from "@/@types";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Tooltip,
5 | TooltipContent,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip";
8 | import { useLanguageContext } from "@/contexts/I18N";
9 | import { openLink } from "@/lib";
10 |
11 | const SettingsLinkGroup = ({ links }: { links: Array }) => {
12 | const { t } = useLanguageContext();
13 |
14 | return (
15 |
16 | {links.map(({ icon, title, url }) => (
17 |
18 |
19 |
27 |
28 | {t(title)}
29 |
30 | ))}
31 |
32 | );
33 | };
34 |
35 | export default SettingsLinkGroup;
36 |
--------------------------------------------------------------------------------
/src/features/settings/components/section.tsx:
--------------------------------------------------------------------------------
1 | import { H3, TypographyMuted } from "@/components/ui/typography";
2 | import { useLanguageContext } from "@/contexts/I18N";
3 | import { cn } from "@/lib";
4 | import { HTMLAttributes, PropsWithChildren } from "react";
5 |
6 | interface Props extends HTMLAttributes {
7 | title?: string;
8 | description?: string;
9 | }
10 |
11 | export const SettingsSection = ({
12 | children,
13 | title,
14 | description,
15 | className,
16 | ...props
17 | }: PropsWithChildren) => {
18 | const { t } = useLanguageContext();
19 |
20 | return (
21 |
28 | {(title || description) && (
29 |
30 | {title &&
{t("settings.settings." + title)}
}
31 | {description && (
32 |
33 | {t("settings.settings." + description)}
34 |
35 | )}
36 |
37 | )}
38 | {children}
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/features/settings/components/settingsItem.tsx:
--------------------------------------------------------------------------------
1 | import { Label } from "@/components/ui/label";
2 | import { useLanguageContext } from "@/contexts/I18N";
3 | import { PropsWithChildren } from "react";
4 |
5 | type Props = {
6 | title: string;
7 | };
8 |
9 | export const SettingsItem = ({ title, children }: PropsWithChildren) => {
10 | const { t } = useLanguageContext();
11 |
12 | return (
13 |
14 |
15 |
16 | {children}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/features/settings/components/tab.tsx:
--------------------------------------------------------------------------------
1 | import { P } from "@/components/ui/typography";
2 | import { cn } from "@/lib/utils";
3 | import { JSX } from "react";
4 |
5 | interface SettingTabProps {
6 | title: string;
7 | icon: JSX.Element;
8 | isActive: boolean;
9 | onClick: () => void;
10 | }
11 |
12 | const SettingTab = ({ icon, title, isActive, onClick }: SettingTabProps) => {
13 | return (
14 |
28 | );
29 | };
30 |
31 | export default SettingTab;
32 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/container.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react";
2 |
3 | const SettingsContainer = ({ children }: PropsWithChildren) => {
4 | return (
5 | {children}
6 | );
7 | };
8 |
9 | export default SettingsContainer;
10 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/index.tsx:
--------------------------------------------------------------------------------
1 | import { useLanguageContext } from "@/contexts/I18N";
2 | import { SettingsSection } from "../../section";
3 | import SettingTitle from "../../title";
4 | import SettingsContainer from "../container";
5 | import LogDisplay from "./settings/logDisplay";
6 |
7 | const DeveloperSettings = () => {
8 | const { t } = useLanguageContext();
9 |
10 | return (
11 |
12 | {t("settings.titles.developer")}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default DeveloperSettings;
24 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { LogEntry } from "@/@types/logs";
2 | import {
3 | ConsoleErrorDisplay,
4 | ConsoleInfoDisplay,
5 | ConsoleWarningDisplay,
6 | } from "./logTypes";
7 |
8 | const LogSwitch = ({ message, timestamp, level }: LogEntry) => {
9 | switch (level) {
10 | case "error":
11 | return (
12 |
17 | );
18 | case "warn":
19 | return (
20 |
25 | );
26 |
27 | case "info":
28 | return (
29 |
34 | );
35 |
36 | default:
37 | return (
38 |
43 | );
44 | }
45 | };
46 |
47 | export default LogSwitch;
48 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logTypes/base.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tooltip,
3 | TooltipContent,
4 | TooltipTrigger,
5 | } from "@/components/ui/tooltip";
6 | import { cn } from "@/lib";
7 | import { format } from "date-fns";
8 | import { HTMLAttributes, PropsWithChildren } from "react";
9 |
10 | interface Props extends HTMLAttributes {
11 | timestamp?: number;
12 | }
13 |
14 | const BaseLog = ({
15 | children,
16 | timestamp,
17 | className,
18 | }: PropsWithChildren) => {
19 | return (
20 |
21 |
22 |
28 | {children}
29 |
30 |
31 |
32 | {!!timestamp && (
33 |
34 | {format(new Date(timestamp), "yyyy-MM-dd HH:mm:ss")}
35 |
36 | )}
37 |
38 | );
39 | };
40 |
41 | export { BaseLog };
42 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logTypes/error.tsx:
--------------------------------------------------------------------------------
1 | import { LogEntry } from "@/@types/logs";
2 | import { Ban } from "lucide-react";
3 | import { JSX } from "react";
4 | import { BaseLog } from "./base";
5 | import { TypographyMuted } from "@/components/ui/typography";
6 |
7 | interface ConsoleErrorDisplayProps {
8 | customIcon?: JSX.Element;
9 | // title: string;
10 | description: string;
11 | timestamp?: LogEntry["timestamp"];
12 | }
13 |
14 | const ConsoleErrorDisplay = ({
15 | description,
16 | customIcon,
17 | timestamp,
18 | }: ConsoleErrorDisplayProps) => {
19 | return (
20 |
21 | {customIcon ? customIcon : }
22 | {description}
23 |
24 | );
25 | };
26 |
27 | export { ConsoleErrorDisplay };
28 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logTypes/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./base";
2 | export * from "./error";
3 | export * from "./info";
4 | export * from "./warn";
5 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logTypes/info.tsx:
--------------------------------------------------------------------------------
1 | import { LogEntry } from "@/@types/logs";
2 | import { Info } from "lucide-react";
3 | import { JSX } from "react";
4 | import { BaseLog } from "./base";
5 | import { TypographyMuted } from "@/components/ui/typography";
6 |
7 | interface ConsoleInfoDisplayProps {
8 | customIcon?: JSX.Element;
9 | // title: string;
10 | description: string;
11 | timestamp?: LogEntry["timestamp"];
12 | }
13 |
14 | const ConsoleInfoDisplay = ({
15 | description,
16 | customIcon,
17 | timestamp,
18 | }: ConsoleInfoDisplayProps) => {
19 | return (
20 |
21 |
22 | {customIcon ? customIcon : }
23 |
24 | {description}
25 |
26 | );
27 | };
28 |
29 | export { ConsoleInfoDisplay };
30 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logTypes/warn.tsx:
--------------------------------------------------------------------------------
1 | import { LogEntry } from "@/@types/logs";
2 | import { CircleAlert } from "lucide-react";
3 | import { JSX } from "react";
4 | import { BaseLog } from "./base";
5 | import { TypographyMuted } from "@/components/ui/typography";
6 |
7 | interface ConsoleWarningDisplayProps {
8 | customIcon?: JSX.Element;
9 | // title: string;
10 | description: string;
11 | timestamp?: LogEntry["timestamp"];
12 | }
13 |
14 | const ConsoleWarningDisplay = ({
15 | description,
16 | customIcon,
17 | timestamp,
18 | }: ConsoleWarningDisplayProps) => {
19 | return (
20 |
21 |
22 | {customIcon ? customIcon : }
23 |
24 | {description}
25 |
26 | );
27 | };
28 |
29 | export { ConsoleWarningDisplay };
30 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/developer/settings/logDisplay/logWindow.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollArea } from "@/components/ui/scroll-area";
2 | import { useLogger } from "@/hooks";
3 | import { cn } from "@/lib";
4 | import { useQuery } from "@tanstack/react-query";
5 | import LogSwitch from "./logSwitch";
6 | import { H5 } from "@/components/ui/typography";
7 |
8 | interface LogWindowProps {
9 | enabled: boolean;
10 | }
11 |
12 | const LogWindow = ({ enabled }: LogWindowProps) => {
13 | const { logs, retrieveLogs } = useLogger();
14 |
15 | useQuery({
16 | queryKey: ["logs"],
17 | queryFn: async () => {
18 | await retrieveLogs();
19 | return null;
20 | },
21 | enabled: enabled,
22 | });
23 |
24 | return (
25 |
26 |
32 |
33 | {logs?.length ? (
34 | logs.map((log, i) => {
35 | return
;
36 | })
37 | ) : (
38 |
39 |
No logs
40 |
41 | )}
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default LogWindow;
49 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/miscellaneous.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { useLanguageContext } from "@/contexts/I18N";
3 | import { SettingsSection } from "../section";
4 | import { SettingsItem } from "../settingsItem";
5 | import SettingTitle from "../title";
6 | import SettingsContainer from "./container";
7 |
8 | const MiscellaneousSettings = () => {
9 | const { t } = useLanguageContext();
10 |
11 | const handleResetCache = async () => {
12 | console.log("Resetting IGDB cache");
13 | localStorage.removeItem("igdb_access_token");
14 | localStorage.removeItem("igdb_token_expiration");
15 |
16 | window.location.reload();
17 | };
18 |
19 | return (
20 |
21 | {t("settings.titles.miscellaneous")}
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default MiscellaneousSettings;
37 |
--------------------------------------------------------------------------------
/src/features/settings/components/tabs/plugins/addButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Dialog, DialogTrigger } from "@/components/ui/dialog";
3 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
4 | import { useLanguageContext } from "@/contexts/I18N";
5 | import { Plus } from "lucide-react";
6 | import AddPluginModal from "./addPluginModal";
7 | import { Dispatch, SetStateAction } from "react";
8 |
9 | interface PluginAddButtonProps {
10 | open: boolean;
11 | setOpen: Dispatch>;
12 | }
13 |
14 | const PluginAddButton = ({ open, setOpen }: PluginAddButtonProps) => {
15 | const { t } = useLanguageContext();
16 |
17 | return (
18 | <>
19 | {/* Desktop Add Button (Text + Icon) */}
20 |
36 |
37 |
38 | >
39 | );
40 | };
41 |
42 | export default PluginAddButton;
--------------------------------------------------------------------------------
/src/features/settings/components/title.tsx:
--------------------------------------------------------------------------------
1 | import { H3 } from "@/components/ui/typography";
2 | import { cn } from "@/lib";
3 | import { HTMLAttributes } from "react";
4 |
5 | interface SettingTitleProps extends HTMLAttributes {
6 | children: string;
7 | }
8 |
9 | const SettingTitle = ({ children, className, ...props }: SettingTitleProps) => {
10 | return (
11 |
12 |
{children}
13 |
14 | );
15 | };
16 |
17 | export default SettingTitle;
18 |
--------------------------------------------------------------------------------
/src/features/splashscreen/components/index.tsx:
--------------------------------------------------------------------------------
1 | import bgImage from "@/assets/bg.png";
2 | import logoImage from "@/assets/icon.png";
3 | import "../styles.css";
4 |
5 | const SplashScreen = () => {
6 | return (
7 |
8 |
9 |
10 |

11 |
12 |
13 |
14 |

15 |
16 |
17 | );
18 | };
19 |
20 | export default SplashScreen;
21 |
--------------------------------------------------------------------------------
/src/features/splashscreen/styles.css:
--------------------------------------------------------------------------------
1 | .splash-bg {
2 | width: 100%;
3 | height: 100%;
4 | position: absolute;
5 | z-index: -1;
6 | inset: 0;
7 | }
8 |
9 | .splash-bg img {
10 | width: 100%;
11 | height: 100%;
12 | object-fit: cover;
13 | filter: blur(2px);
14 | }
15 |
16 | .splash-bg span {
17 | position: absolute;
18 | inset: 0;
19 | z-index: 2;
20 | opacity: 0.95;
21 | background-color: hsl(222.2, 84%, 4.9%);
22 | }
23 |
24 | .splash-main {
25 | position: absolute;
26 | top: 50%;
27 | left: 50%;
28 | width: 200px; /* Adjust as needed */
29 | height: 200px; /* Adjust as needed */
30 | transform: translate(-50%, -50%);
31 | overflow: hidden; /* Ensure the pseudo-element does not overflow */
32 | border-radius: 28px; /* Adjust for desired roundness */
33 | }
34 |
35 | .splash-main img {
36 | width: 100%;
37 | height: 100%;
38 | object-fit: cover;
39 | position: relative; /* Needed for the pseudo-element positioning */
40 | overflow: hidden;
41 | }
42 |
43 | .splash-main::before {
44 | content: "";
45 | position: absolute;
46 | top: -100%;
47 | left: -100%;
48 | width: 200%;
49 | height: 200%;
50 | z-index: 55;
51 | background: linear-gradient(
52 | 135deg,
53 | rgba(255, 255, 255, 0) 49%,
54 | rgba(255, 255, 255, 1) 50%,
55 | rgba(255, 255, 255, 0) 51%
56 | );
57 | opacity: 0.5;
58 | animation: loadingEffect 3s linear infinite;
59 | }
60 |
61 | @keyframes loadingEffect {
62 | 0% {
63 | transform: translateX(0) translateY(0);
64 | }
65 | 100% {
66 | transform: translateX(100%) translateY(100%);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/features/torBox/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import { getTorBoxUserInstance } from "@/lib/api/torbox/user";
2 |
3 | export const obtainTorBoxUser = async (api_key: string) => {
4 | const torBoxUser = getTorBoxUserInstance(api_key);
5 | return await torBoxUser.getUserInfo();
6 | };
7 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useAppStartup";
2 | export * from "./useDebounce";
3 | export * from "./useLogger";
4 | export * from "./useMapState";
5 | export * from "./usePluginActions";
6 | export * from "./usePlugins";
7 | export * from "./useProtonDb";
8 | export * from "./useSetState";
9 | export * from "./useSettings";
10 | export * from "./useThemes";
11 | export * from "./useUpdater";
12 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from "react";
2 |
3 | export const useDebounce = (
4 | callback: (...args: any[]) => void,
5 | delay: number
6 | ) => {
7 | const timer = useRef | null>(null);
8 |
9 | const debouncedCallback = useCallback(
10 | (...args: any[]) => {
11 | if (timer.current) {
12 | clearTimeout(timer.current);
13 | }
14 |
15 | timer.current = setTimeout(() => {
16 | callback(...args);
17 | timer.current = null;
18 | }, delay);
19 | },
20 | [callback, delay]
21 | );
22 |
23 | useEffect(() => {
24 | return () => {
25 | if (timer.current) {
26 | clearTimeout(timer.current);
27 | }
28 | };
29 | }, []);
30 |
31 | return debouncedCallback;
32 | };
33 |
--------------------------------------------------------------------------------
/src/hooks/useLogger.ts:
--------------------------------------------------------------------------------
1 | import { useLoggerStore } from "@/stores/logger";
2 | import { useEffect } from "react";
3 |
4 | export const useLogger = () => {
5 | const {
6 | logs,
7 | loading,
8 | error,
9 | fetchLogs,
10 | clear,
11 | log,
12 | getLog,
13 | filter,
14 | getLoggedDates,
15 | } = useLoggerStore();
16 |
17 | useEffect(() => {
18 | if (!fetchLogs) return;
19 |
20 | fetchLogs();
21 | }, [fetchLogs]);
22 |
23 | return {
24 | logs,
25 | loading,
26 | error,
27 | addLog: log,
28 | retrieveLogs: fetchLogs,
29 | removeLogs: clear,
30 | getLog,
31 | filter,
32 | getLoggedDates,
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/src/hooks/useMapState.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | // Hook for managing Map state
4 | export const useMapState = (initialState: [K, V][] = []) => {
5 | const [map, setMap] = useState