Revert "Update to 3.1.2"

This reverts commit c2b4805792.
This commit is contained in:
mrcanelas
2025-02-01 14:39:33 -03:00
parent 687e4c7c6d
commit 0c74237fa3
112 changed files with 2059 additions and 9351 deletions
-4
View File
@@ -1,4 +0,0 @@
node_modules
pnpm-lock.yaml
dist
.git
-40
View File
@@ -1,40 +0,0 @@
# Etapa de construção do frontend
FROM node:18-alpine AS builder
# Define o diretório de trabalho
WORKDIR /app
# Copia os arquivos necessários
COPY package.json package-lock.json ./
# Instala as dependências
RUN npm install -g pnpm && pnpm install --no-frozen-lockfile
# Copia o restante do código
COPY . .
# Compila o frontend
RUN pnpm run build
# Etapa de execução do backend
FROM node:18-alpine AS runner
# Define o diretório de trabalho
WORKDIR /app
# Copia apenas os arquivos necessários para o backend
COPY --from=builder /app/addon /app/addon
COPY --from=builder /app/package.json ./
COPY --from=builder /app/package-lock.json ./
# Instala apenas as dependências de produção
RUN npm install -g pnpm && pnpm install --no-frozen-lockfile
# Copia os arquivos do frontend para serem servidos pelo backend
COPY --from=builder /app/dist /app/addon/static
# Exposição da porta do backend
EXPOSE 7000
# Inicia o servidor Express
CMD ["node", "addon/server.js"]
-20
View File
@@ -1,20 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Movie Database Addon - Stremio Addon</title>
<link rel="shortcut icon" href="https://github.com/mrcanelas/tmdb-addon/raw/main/images/favicon.png"
type="image/x-icon" />
</head>
<iframe src="https://tmdb-addon-config-page.vercel.app"
style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;"></iframe>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 B

-25
View File
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Streaming Catalogs</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-S47YFG3SDZ"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-S47YFG3SDZ');
</script>
<script type="module" crossorigin src="/assets/index.0f7d9069.js"></script>
<link rel="stylesheet" href="/assets/index.79bc1b06.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

-42
View File
@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
-79
View File
@@ -1,79 +0,0 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Home from "./pages/Home";
import Catalogs from "./pages/Catalogs";
import Integrations from "./pages/Integrations";
import Others from "./pages/Others";
import NotFound from "./pages/NotFound";
import { Sidebar } from "./components/Sidebar";
import { Header } from "./components/Header";
import { useState } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ConfigProvider } from "@/contexts/ConfigContext";
const queryClient = new QueryClient();
type Page = "home" | "catalogs" | "integrations" | "others";
const Layout = ({ children }: { children: React.ReactNode }) => {
const [isOpen, setIsOpen] = useState(false);
const [currentPage, setCurrentPage] = useState<Page>("home");
const toggleSidebar = () => {
setIsOpen(!isOpen);
};
const renderPage = () => {
switch (currentPage) {
case "home":
return <Home />;
case "catalogs":
return <Catalogs />;
case "integrations":
return <Integrations />;
case "others":
return <Others />;
default:
return <NotFound />;
}
};
const isHome = currentPage === "home";
return (
<div className="min-h-screen bg-gray-100">
<Sidebar
isOpen={isOpen}
setIsOpen={setIsOpen}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
<div className="flex-1 flex flex-col md:pl-64 h-screen">
{(!isHome || window.innerWidth < 768) && (
<Header isOpen={isOpen} toggleSidebar={toggleSidebar} isHome={isHome} />
)}
<ScrollArea className="flex-1 px-4 sm:px-6 md:px-8 lg:px-12">
{renderPage()}
</ScrollArea>
</div>
</div>
);
};
const App = () => (
<ConfigProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
<Sonner />
<Layout>
<Home />
</Layout>
</TooltipProvider>
</QueryClientProvider>
</ConfigProvider>
);
export default App;
@@ -1,9 +0,0 @@
export default function DefaultIntegration() {
return (
<div className="text-center py-3 sm:py-4">
<p className="text-sm sm:text-base text-muted-foreground">
Configuration options for this integration will be available soon.
</p>
</div>
);
}
-45
View File
@@ -1,45 +0,0 @@
import { Menu, X } from "lucide-react";
import { useEffect, useState } from "react";
interface HeaderProps {
isOpen: boolean;
toggleSidebar: () => void;
isHome: boolean;
}
export function Header({ isOpen, toggleSidebar, isHome }: HeaderProps) {
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const isTransparent = isHome && isMobile;
return (
<header className={`flex justify-between items-center p-6 z-10 ${isTransparent ? 'bg-transparent' : 'bg-white'}`}>
<div className="flex items-center gap-4">
{isMobile && (
<button
onClick={toggleSidebar}
className={`inline-flex items-center justify-center p-2 rounded-md ${isHome ? ' text-white hover:text-gray-100' : 'text-sidebar hover:bg-gray-100'} shadow-sm`}
>
<Menu size={24} />
</button>
)}
</div>
<div className={`flex items-center gap-4 ${isHome ? 'hidden' : 'block'}`}>
<img
src="https://ui-avatars.com/api/?name=silas-alves"
alt="User"
className="inline-block w-10 h-10 rounded-full"
/>
</div>
</header>
);
}
@@ -1,45 +0,0 @@
import {
Dialog,
DialogContent,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { IntegrationDialog } from "./IntegrationDialog";
interface IntegrationCardProps {
id: string;
name: string;
icon: string;
description: string;
}
export function IntegrationCard({ id, name, icon, description }: IntegrationCardProps) {
return (
<Dialog>
<DialogTrigger asChild>
<Card className="p-4 sm:p-6 hover:shadow-lg transition-shadow cursor-pointer h-full">
<div className="flex flex-col h-full">
<div className="flex flex-col items-center text-center flex-1">
<img src={icon} alt={name} className="w-10 h-10 sm:w-12 sm:h-12 mb-3 sm:mb-4" />
<h3 className="font-semibold mb-1.5 sm:mb-2 text-base sm:text-lg">{name}</h3>
<p className="text-xs sm:text-sm text-gray-500">{description}</p>
</div>
<div className="flex justify-center mt-4">
<Button
variant="ghost"
className="text-primary hover:text-primary/80 font-medium text-sm sm:text-base px-3 py-1.5 sm:px-4 sm:py-2"
>
Setup
</Button>
</div>
</div>
</Card>
</DialogTrigger>
<DialogContent className="w-[95%] sm:w-auto">
<IntegrationDialog id={id} name={name} icon={icon} />
</DialogContent>
</Dialog>
);
}
@@ -1,40 +0,0 @@
import { lazy, Suspense } from "react";
import { DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import DefaultIntegration from "./DefaultIntegration";
interface IntegrationDialogProps {
id: string;
name: string;
icon: string;
}
export function IntegrationDialog({ id, name, icon }: IntegrationDialogProps) {
const IntegrationComponent = lazy(() =>
/* @vite-ignore */
import(`../integrations/${id}.tsx`).catch(() => {
console.error(`Failed to load integration component for ${id}`);
return import("./DefaultIntegration");
})
);
return (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-lg sm:text-xl">
<img src={icon} alt={name} className="w-5 h-5 sm:w-6 sm:h-6" />
{name} Configuration
</DialogTitle>
<DialogDescription className="text-sm sm:text-base">
Configure your {name} integration settings below.
</DialogDescription>
</DialogHeader>
<div className="grid gap-3 sm:gap-4">
<Suspense fallback={<Skeleton className="h-[200px] w-full" />}>
<IntegrationComponent />
</Suspense>
</div>
</>
);
}
-113
View File
@@ -1,113 +0,0 @@
import { Home, GalleryVerticalEnd, Puzzle, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { KoFiDialog } from "react-kofi";
import "react-kofi/dist/styles.css";
import "@/styles/kofi-dialog.css";
import { generateAddonUrl } from "@/lib/config";
import { useConfig } from "@/contexts/ConfigContext";
type Page = "home" | "catalogs" | "integrations" | "others";
const menuItems = [
{ icon: Home, label: "Home", id: "home" as Page },
{ icon: GalleryVerticalEnd, label: "Catalogs", id: "catalogs" as Page },
{ icon: Puzzle, label: "Integrations", id: "integrations" as Page },
{ icon: Settings, label: "Others", id: "others" as Page },
];
interface SidebarProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
currentPage: Page;
setCurrentPage: (page: Page) => void;
}
export function Sidebar({ isOpen, setIsOpen, currentPage, setCurrentPage }: SidebarProps) {
const config = useConfig();
const handleInstall = () => {
const url = generateAddonUrl({
rpdbkey: config.rpdbkey,
includeAdult: config.includeAdult,
provideImdbId: config.provideImdbId,
tmdbPrefix: config.tmdbPrefix,
language: config.language,
sessionId: config.sessionId,
catalogs: config.catalogs
});
const stremioUrl = url.replace(/^https?:\/\//, 'stremio://');
window.location.href = stremioUrl;
};
return (
<>
<div
className={cn(
"fixed inset-y-0 left-0 z-40 bg-sidebar transform transition-transform duration-200 ease-in-out",
isOpen ? "translate-x-0" : "-translate-x-full",
"md:translate-x-0"
)}
>
<div className="flex flex-col min-h-screen py-6 space-y-10">
<div className="flex items-center gap-2 mx-6 mt-10">
<img
src="https://www.themoviedb.org/assets/2/v4/logos/v2/blue_short-8e7b30f73a4020692ccca9c88bafe5dcb6f8a62a4c6bc55cd9ba82bb2cd95f6c.svg"
alt="TMDB Logo"
className="w-60"
/>
</div>
<nav className="flex-1">
<ul>
{menuItems.map((item) => (
<li key={item.label}>
<button
onClick={() => {
setCurrentPage(item.id);
setIsOpen(false);
}}
className={cn(
"flex items-center w-full px-6 py-2 mt-4 text-gray-500 hover:bg-gray-700 hover:bg-opacity-25 hover:text-gray-100",
currentPage === item.id &&
"bg-gray-700 text-gray-100"
)}
>
<item.icon className="w-5 h-5 mr-2" />
{item.label}
</button>
</li>
))}
</ul>
</nav>
<div className="p-6 grid place-items-center">
<button
className="w-full mb-3 bg-primary text-white rounded-lg py-2.5 px-4 hover:bg-primary/90 transition-colors"
onClick={handleInstall}
>
Install
</button>
<KoFiDialog
color="#01b4e4"
textColor="#fff"
id="mrcanelas"
label="Support me"
padding={6}
iframe={false}
buttonRadius="6px"
className="w-full"
/>
</div>
</div>
</div>
{isOpen && (
<div
className="fixed inset-0 bg-black/50 z-30 md:hidden"
onClick={() => setIsOpen(false)}
/>
)}
</>
);
}
-59
View File
@@ -1,59 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }
-36
View File
@@ -1,36 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
-56
View File
@@ -1,56 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
-79
View File
@@ -1,79 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
-120
View File
@@ -1,120 +0,0 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
-22
View File
@@ -1,22 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
-24
View File
@@ -1,24 +0,0 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
@@ -1,46 +0,0 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
-158
View File
@@ -1,158 +0,0 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
-15
View File
@@ -1,15 +0,0 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }
-29
View File
@@ -1,29 +0,0 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }
-27
View File
@@ -1,27 +0,0 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
-127
View File
@@ -1,127 +0,0 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
-33
View File
@@ -1,33 +0,0 @@
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
-28
View File
@@ -1,28 +0,0 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
-105
View File
@@ -1,105 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
import { ConfigContext, type ConfigContextType, type CatalogConfig } from "./config";
export function ConfigProvider({ children }: { children: React.ReactNode }) {
const [rpdbkey, setRpdbkey] = useState(() => {
return localStorage.getItem("rpdbkey") || "";
});
const [includeAdult, setIncludeAdult] = useState(() => {
return localStorage.getItem("includeAdult") === "true";
});
const [provideImdbId, setProvideImdbId] = useState(() => {
return localStorage.getItem("provideImdbId") === "true";
});
const [tmdbPrefix, setTmdbPrefix] = useState(() => {
return localStorage.getItem("tmdbPrefix") === "true";
});
const [language, setLanguage] = useState(() => {
return localStorage.getItem("language") || "en-US";
});
const [sessionId, setSessionId] = useState(() => {
return localStorage.getItem("sessionId") || "";
});
const [streaming, setStreaming] = useState<string[]>(() => {
const saved = localStorage.getItem("streaming");
return saved ? JSON.parse(saved) : [];
});
const [catalogs, setCatalogs] = useState<CatalogConfig[]>(() => {
const saved = localStorage.getItem("catalogs");
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem("rpdbkey", rpdbkey);
}, [rpdbkey]);
useEffect(() => {
localStorage.setItem("includeAdult", String(includeAdult));
}, [includeAdult]);
useEffect(() => {
localStorage.setItem("provideImdbId", String(provideImdbId));
}, [provideImdbId]);
useEffect(() => {
localStorage.setItem("tmdbPrefix", String(tmdbPrefix));
}, [tmdbPrefix]);
useEffect(() => {
localStorage.setItem("language", language);
}, [language]);
useEffect(() => {
localStorage.setItem("sessionId", sessionId);
}, [sessionId]);
useEffect(() => {
localStorage.setItem("streaming", JSON.stringify(streaming));
}, [streaming]);
useEffect(() => {
localStorage.setItem("catalogs", JSON.stringify(catalogs));
}, [catalogs]);
return (
<ConfigContext.Provider
value={{
rpdbkey,
includeAdult,
provideImdbId,
tmdbPrefix,
language,
sessionId,
streaming,
catalogs,
setRpdbkey,
setIncludeAdult,
setProvideImdbId,
setTmdbPrefix,
setLanguage,
setSessionId,
setStreaming,
setCatalogs,
}}
>
{children}
</ConfigContext.Provider>
);
}
export function useConfig() {
const context = useContext(ConfigContext);
if (context === undefined) {
throw new Error("useConfig must be used within a ConfigProvider");
}
return context;
}
-29
View File
@@ -1,29 +0,0 @@
import { createContext } from 'react';
export interface CatalogConfig {
id: string;
type: "movie" | "series";
enabled: boolean;
showInHome: boolean;
}
export interface ConfigContextType {
rpdbkey: string;
includeAdult: boolean;
provideImdbId: boolean;
tmdbPrefix: boolean;
language: string;
sessionId: string;
streaming: string[];
catalogs: CatalogConfig[];
setRpdbkey: (value: string) => void;
setIncludeAdult: (value: boolean) => void;
setProvideImdbId: (value: boolean) => void;
setTmdbPrefix: (value: boolean) => void;
setLanguage: (value: string) => void;
setSessionId: (value: string) => void;
setStreaming: (value: string[]) => void;
setCatalogs: (value: CatalogConfig[] | ((prev: CatalogConfig[]) => CatalogConfig[])) => void;
}
export const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
-127
View File
@@ -1,127 +0,0 @@
export interface ExtraOption {
name: string;
options?: string[];
isRequired?: boolean;
}
export interface Catalog {
id: string;
name: string;
type: "movie" | "series";
extraRequired?: boolean;
}
// Catálogos base que sempre estarão disponíveis
export const baseCatalogs: Catalog[] = [
{ id: "tmdb.top", name: "Popular", type: "movie" },
{ id: "tmdb.top", name: "Popular", type: "series" },
{ id: "tmdb.year", name: "Year", type: "movie" },
{ id: "tmdb.year", name: "Year", type: "series" },
{ id: "tmdb.language", name: "Language", type: "movie" },
{ id: "tmdb.language", name: "Language", type: "series" },
{ id: "tmdb.trending", name: "Trending", type: "movie" },
{ id: "tmdb.trending", name: "Trending", type: "series" },
];
// Catálogos que requerem autenticação
export const authCatalogs: Catalog[] = [
{ id: "tmdb.favorites", name: "Favorites", type: "movie" },
{ id: "tmdb.favorites", name: "Favorites", type: "series" },
{ id: "tmdb.watchlist", name: "Watchlist", type: "movie" },
{ id: "tmdb.watchlist", name: "Watchlist", type: "series" },
];
// Catálogos de streaming
export const streamingCatalogs: Record<string, Catalog[]> = {
nfx: [
{ id: "streaming.nfx", name: "Netflix", type: "movie" },
{ id: "streaming.nfx", name: "Netflix", type: "series" }
],
nfk: [
{ id: "streaming.nfk", name: "Netflix Kids", type: "movie" },
{ id: "streaming.nfk", name: "Netflix Kids", type: "series" }
],
hbm: [
{ id: "streaming.hbm", name: "HBO Max", type: "movie" },
{ id: "streaming.hbm", name: "HBO Max", type: "series" }
],
dnp: [
{ id: "streaming.dnp", name: "Disney+", type: "movie" },
{ id: "streaming.dnp", name: "Disney+", type: "series" }
],
amp: [
{ id: "streaming.amp", name: "Prime Video", type: "movie" },
{ id: "streaming.amp", name: "Prime Video", type: "series" }
],
atp: [
{ id: "streaming.atp", name: "Apple TV+", type: "movie" },
{ id: "streaming.atp", name: "Apple TV+", type: "series" }
],
pmp: [
{ id: "streaming.pmp", name: "Paramount+", type: "movie" },
{ id: "streaming.pmp", name: "Paramount+", type: "series" }
],
pcp: [
{ id: "streaming.pcp", name: "Peacock Premium", type: "movie" },
{ id: "streaming.pcp", name: "Peacock Premium", type: "series" }
],
hlu: [
{ id: "streaming.hlu", name: "Hulu", type: "movie" },
{ id: "streaming.hlu", name: "Hulu", type: "series" }
],
cts: [
{ id: "streaming.cts", name: "Curiosity Stream", type: "movie" },
{ id: "streaming.cts", name: "Curiosity Stream", type: "series" }
],
mgl: [
{ id: "streaming.mgl", name: "MagellanTV", type: "movie" },
{ id: "streaming.mgl", name: "MagellanTV", type: "series" }
],
cru: [
{ id: "streaming.cru", name: "Crunchyroll", type: "movie" },
{ id: "streaming.cru", name: "Crunchyroll", type: "series" }
],
hay: [
{ id: "streaming.hay", name: "Hayu", type: "series" }
],
clv: [
{ id: "streaming.clv", name: "Clarovideo", type: "movie" },
{ id: "streaming.clv", name: "Clarovideo", type: "series" }
],
gop: [
{ id: "streaming.gop", name: "Globoplay", type: "movie" },
{ id: "streaming.gop", name: "Globoplay", type: "series" }
],
hst: [
{ id: "streaming.hst", name: "Hotstar", type: "movie" },
{ id: "streaming.hst", name: "Hotstar", type: "series" }
],
zee: [
{ id: "streaming.zee", name: "Zee5", type: "movie" },
{ id: "streaming.zee", name: "Zee5", type: "series" }
],
nlz: [
{ id: "streaming.nlz", name: "NLZIET", type: "movie" },
{ id: "streaming.nlz", name: "NLZIET", type: "series" }
],
vil: [
{ id: "streaming.vil", name: "Videoland", type: "movie" },
{ id: "streaming.vil", name: "Videoland", type: "series" }
],
sst: [
{ id: "streaming.sst", name: "SkyShowtime", type: "movie" },
{ id: "streaming.sst", name: "SkyShowtime", type: "series" }
],
blv: [
{ id: "streaming.blv", name: "BluTV", type: "movie" },
{ id: "streaming.blv", name: "BluTV", type: "series" }
],
cpd: [
{ id: "streaming.cpd", name: "Canal+", type: "movie" },
{ id: "streaming.cpd", name: "Canal+", type: "series" }
],
dpe: [
{ id: "streaming.dpe", name: "Discovery+", type: "movie" },
{ id: "streaming.dpe", name: "Discovery+", type: "series" }
]
};
-39
View File
@@ -1,39 +0,0 @@
interface Integration {
id: string;
name: string;
icon: string;
description: string;
}
export const integrations: Integration[] = [
{
id: "tmdb",
name: "TMDB Lists",
icon: "https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_2-d537fb228cf3ded904ef09b136fe3fec72548ebc1fea3fbbd1ad9e36364db38b.svg",
description: "Sync your TMDB lists to discover new movies and TV shows.",
},
{
id: "rpdb",
name: "Rating Poster Database",
icon: "https://github.com/RatingPosterDB.png",
description: "Add ratings and scores to your movies and TV shows posters.",
},
{
id: "streaming",
name: "Streaming Catalogs",
icon: "https://www.svgrepo.com/show/303341/netflix-1-logo.svg",
description: "Set up your streaming services to see content availability. Based on rleroi/Stremio-Streaming-Catalogs-Addon.",
},
{
id: "mdblist",
name: "MDBList",
icon: "https://mdblist.com/static/mdblist.png",
description: "Integrate your MDBList lists to expand your content library.",
},
{
id: "trakt",
name: "Trakt",
icon: "https://trakt.tv/assets/logos/logomark.square.gradient-b644b16c38ff775861b4b1f58c1230f6a097a2466ab33ae00445a505c33fcb91.svg",
description: "Track what you watch and sync your progress with Trakt.tv.",
}
];
-50
View File
@@ -1,50 +0,0 @@
export const streamingServices = [
{ id: "nfx", name: "Netflix", icon: "/streaming/netflix.webp" },
{ id: "nfk", name: "Netflix Kids", icon: "/streaming/netflixkids.webp" },
{ id: "hbm", name: "HBO Max", icon: "/streaming/hbo.webp" },
{ id: "dnp", name: "Disney+", icon: "/streaming/disney.webp" },
{ id: "amp", name: "Prime Video", icon: "/streaming/prime.webp" },
{ id: "atp", name: "Apple TV+", icon: "/streaming/apple.webp" },
{ id: "pmp", name: "Paramount+", icon: "/streaming/paramount.webp" },
{ id: "pcp", name: "Peacock Premium", icon: "/streaming/peacock.webp" },
{ id: "hlu", name: "Hulu", icon: "/streaming/hulu.webp" },
{ id: "cts", name: "Curiosity Stream", icon: "/streaming/curiositystream.webp" },
{ id: "mgl", name: "MagellanTV", icon: "/streaming/magellan.webp" },
{ id: "cru", name: "Crunchyroll", icon: "/streaming/crunchyroll.webp" },
{ id: "hay", name: "Hayu", icon: "/streaming/hayu.webp" },
{ id: "clv", name: "Clarovideo", icon: "/streaming/claro.webp" },
{ id: "gop", name: "Globoplay", icon: "/streaming/globo.webp" },
{ id: "hst", name: "Hotstar", icon: "/streaming/hotstar.webp" },
{ id: "zee", name: "Zee5", icon: "/streaming/zee5.webp" },
{ id: "nlz", name: "NLZIET", icon: "/streaming/nlziet.webp" },
{ id: "vil", name: "Videoland", icon: "/streaming/videoland.webp" },
{ id: "sst", name: "SkyShowtime", icon: "/streaming/skyshowtime.webp" },
{ id: "blv", name: "BluTV", icon: "/streaming/blu.webp" },
{ id: "cpd", name: "Canal+", icon: "/streaming/canal-plus.webp" },
{ id: "dpe", name: "Discovery+", icon: "/streaming/discovery-plus.webp" }
];
export const regions = {
'United States': [
'nfx', 'nfk', 'dnp', 'amp', 'atp', 'hbm', 'cru', 'pmp', 'mgl', 'cts', 'hlu', 'pcp', 'dpe'
],
'Brazil': [
'nfx', 'nfk', 'dnp', 'atp', 'amp', 'pmp', 'hbm', 'cru', 'clv', 'gop', 'mgl', 'cts'
],
'India': [
'hay', 'nfx', 'nfk', 'atp', 'amp', 'cru', 'zee', 'hst', 'mgl', 'cts', 'dpe'
],
'Turkey': [
'nfx', 'nfk', 'dnp', 'atp', 'amp', 'cru', 'blv', 'mgl', 'cts'
],
'Netherlands': [
'nfx', 'nfk', 'dnp', 'amp', 'atp', 'hbm', 'cru', 'hay', 'vil', 'sst', 'mgl', 'cts', 'nlz', 'dpe'
],
'France': [
'nfx', 'nfk', 'dnp', 'amp', 'atp', 'hbm', 'hay', 'cpd'
],
'Any': [
'nfx', 'nfk', 'dnp', 'amp', 'atp', 'hbm', 'pmp', 'hlu', 'pcp', 'clv', 'gop', 'blv',
'zee', 'hst', 'hay', 'vil', 'sst', 'mgl', 'cts', 'cru', 'nlz', 'cpd', 'dpe'
]
};
-19
View File
@@ -1,19 +0,0 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
-191
View File
@@ -1,191 +0,0 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
-25
View File
@@ -1,25 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-['Inter'];
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
-133
View File
@@ -1,133 +0,0 @@
import { useState, useEffect } from "react";
import { useConfig } from "@/contexts/ConfigContext";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { DialogClose } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle, Loader2 } from "lucide-react";
export default function RPDB() {
const { rpdbkey, setRpdbkey } = useConfig();
const [tempKey, setTempKey] = useState(rpdbkey);
const [error, setError] = useState("");
const [isValid, setIsValid] = useState(false);
const [isChecking, setIsChecking] = useState(false);
useEffect(() => {
setTempKey(rpdbkey);
// Se tiver uma chave existente, vamos validá-la
if (rpdbkey) {
validateRPDBKey(rpdbkey);
}
}, [rpdbkey]);
const validateRPDBKey = async (key: string) => {
if (!key) {
setIsValid(false);
setError("");
return false;
}
setIsChecking(true);
try {
const response = await fetch(`https://api.ratingposterdb.com/${key}/isValid`);
const data = await response.json();
if (!(data || {}).valid) {
setError("RPDB Key is invalid, please try again");
setIsValid(false);
return false;
}
setError("");
setIsValid(true);
return true;
} catch (e) {
console.error(e);
setError("Error validating RPDB key");
setIsValid(false);
return false;
} finally {
setIsChecking(false);
}
};
const handleSave = () => {
if (isValid) {
setRpdbkey(tempKey);
}
};
const handleCancel = () => {
setTempKey(rpdbkey);
setError("");
setIsValid(rpdbkey ? true : false);
};
return (
<div className="space-y-6">
<div className="flex flex-col space-y-4">
<div className="space-y-2">
<Label htmlFor="rpdbkey">
RPDB API Key (get it from{" "}
<a
href="https://ratingposterdb.com/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
RatingPosterDB
</a>
)
</Label>
<Input
id="rpdbkey"
value={tempKey}
onChange={(e) => {
setTempKey(e.target.value);
setIsValid(false);
setError("");
}}
placeholder="Enter your RPDB API key"
/>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
<div className="flex justify-end space-x-2">
<DialogClose asChild>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
</DialogClose>
{isValid ? (
<DialogClose asChild>
<Button onClick={handleSave}>
Save Changes
</Button>
</DialogClose>
) : (
<Button
onClick={() => validateRPDBKey(tempKey)}
disabled={!tempKey || isChecking}
>
{isChecking ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Checking
</>
) : (
'Check Key'
)}
</Button>
)}
</div>
</div>
);
}
-110
View File
@@ -1,110 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState, useEffect } from "react";
import { useConfig } from "@/contexts/ConfigContext";
import { DialogClose } from "@/components/ui/dialog";
import { regions, streamingServices } from "@/data/streamings";
export default function Streaming() {
const [selectedCountry, setSelectedCountry] = useState("Brazil");
const { streaming, setStreaming } = useConfig();
const [tempSelectedServices, setTempSelectedServices] = useState<string[]>([]);
useEffect(() => {
setTempSelectedServices(streaming);
}, [streaming]);
const toggleService = (serviceId: string) => {
setTempSelectedServices(prev =>
prev.includes(serviceId)
? prev.filter(id => id !== serviceId)
: [...prev, serviceId]
);
};
const showProvider = (serviceId: string) => {
return regions[selectedCountry as keyof typeof regions]?.includes(serviceId);
};
const handleSave = () => {
setStreaming(tempSelectedServices);
};
const handleCancel = () => {
setTempSelectedServices(streaming);
};
return (
<div className="space-y-6">
<p className="text-xs text-muted-foreground">
Based on <a href="https://github.com/rleroi/Stremio-Streaming-Catalogs-Addon" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">rleroi/Stremio-Streaming-Catalogs-Addon</a>
</p>
<div>
<div className="space-y-6">
<div>
<p className="text-sm text-muted-foreground mb-2">Filter providers by country:</p>
<Select value={selectedCountry} onValueChange={setSelectedCountry}>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-background border shadow-md">
{Object.keys(regions).map((country) => (
<SelectItem
key={country}
value={country}
className="cursor-pointer hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
>
{country}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-5 gap-4">
{streamingServices.map((service) => (
showProvider(service.id) && (
<button
key={service.id}
onClick={() => toggleService(service.id)}
className={`w-12 h-12 sm:w-14 sm:h-14 rounded-xl border transition-opacity ${
tempSelectedServices.includes(service.id)
? "border-primary bg-primary/5"
: "border-border opacity-50 hover:opacity-100"
}`}
title={service.name}
>
<img
src={service.icon}
alt={service.name}
className="w-full h-full rounded-lg object-cover"
/>
</button>
)
))}
</div>
<div className="flex justify-end gap-2">
<DialogClose asChild>
<Button variant="outline" type="button" onClick={handleCancel}>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button type="button" onClick={handleSave}>
Save Changes
</Button>
</DialogClose>
</div>
</div>
</div>
</div>
);
}
-106
View File
@@ -1,106 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import { useConfig } from "@/contexts/ConfigContext";
import { Button } from "@/components/ui/button";
import { DialogClose } from "@/components/ui/dialog";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle, Loader2 } from "lucide-react";
export default function TMDB() {
const { sessionId, setSessionId } = useConfig();
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleRequestToken = useCallback(async (requestToken: string) => {
setIsLoading(true);
try {
const response = await fetch(`https://94c8cb9f702d-tmdb-addon.baby-beamup.club/session_id?request_token=${requestToken}`);
if (!response.ok) throw new Error('Failed to create session');
const sessionId = await response.text();
setSessionId(sessionId);
// Limpa os parâmetros da URL sem mudar a página
window.history.replaceState({}, '', window.location.pathname);
} catch (e) {
console.error(e);
setError("Failed to create TMDB session");
} finally {
setIsLoading(false);
}
}, [setSessionId]);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const requestToken = urlParams.get('request_token');
if (requestToken) {
handleRequestToken(requestToken);
}
}, [handleRequestToken]);
const handleLogin = async () => {
setIsLoading(true);
setError("");
try {
const uuid = crypto.randomUUID();
const response = await fetch(`https://94c8cb9f702d-tmdb-addon.baby-beamup.club/request_token?${uuid}`);
if (!response.ok) throw new Error('Failed to get request token');
const requestToken = await response.text();
const tmdbAuthUrl = `https://www.themoviedb.org/authenticate/${requestToken}?redirect_to=${window.location.href}`;
window.location.href = tmdbAuthUrl;
} catch (e) {
console.error(e);
setError("Failed to start TMDB authentication");
setIsLoading(false);
}
};
const handleLogout = () => {
setSessionId("");
};
return (
<div className="space-y-6">
<div className="flex flex-col space-y-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{sessionId ? (
<div className="flex flex-col items-center space-y-4">
<Alert>
<AlertDescription>
You are logged in to TMDB
</AlertDescription>
</Alert>
<DialogClose asChild>
<Button variant="destructive" onClick={handleLogout}>
Logout
</Button>
</DialogClose>
</div>
) : (
<Button
onClick={handleLogin}
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting to TMDB...
</>
) : (
'Login with TMDB'
)}
</Button>
)}
</div>
</div>
);
}
-46
View File
@@ -1,46 +0,0 @@
interface AddonConfig {
rpdbkey?: string;
includeAdult?: boolean;
provideImdbId?: boolean;
tmdbPrefix?: boolean;
language?: string;
sessionId?: string;
catalogs?: Array<{
id: string;
type: string;
enabled: boolean;
showInHome: boolean;
}>;
}
export function generateAddonUrl(config: AddonConfig): string {
const baseUrl = 'https://94c8cb9f702d-tmdb-addon.baby-beamup.club';
// Criar um novo objeto apenas com os valores necessários
const configToEncode = {
...config,
// Remove os itens se forem nulos/vazios
rpdbkey: config.rpdbkey || undefined,
sessionId: config.sessionId || undefined,
// Filtra apenas catálogos habilitados
catalogs: config.catalogs?.filter(c => c.enabled).map(({ id, type, showInHome }) => ({
id,
type,
showInHome
})) || undefined,
// Converte booleanos para strings
includeAdult: config.includeAdult === true ? "true" : undefined,
provideImdbId: config.provideImdbId === true ? "true" : undefined,
tmdbPrefix: config.tmdbPrefix === true ? "true" : undefined
};
// Remover propriedades undefined/null
const cleanConfig = Object.fromEntries(
Object.entries(configToEncode).filter(([_, value]) => value !== undefined && value !== null)
);
// Converter o objeto em string e codificar para URL
const encodedConfig = encodeURIComponent(JSON.stringify(cleanConfig));
return `${baseUrl}/${encodedConfig}/manifest.json`;
}
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
-5
View File
@@ -1,5 +0,0 @@
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById("root")!).render(<App />);
-205
View File
@@ -1,205 +0,0 @@
import { Switch } from "@/components/ui/switch";
import { Card, CardHeader } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useEffect } from "react";
import { useConfig } from "@/contexts/ConfigContext";
import { integrations } from "@/data/integrations";
import { baseCatalogs, authCatalogs, streamingCatalogs, type Catalog } from "@/data/catalogs";
import { type CatalogConfig } from "@/contexts/config";
import { streamingServices } from "@/data/streamings";
const getIntegrationInfo = (catalogId: string) => {
const [integrationId] = catalogId.split(".");
const integration = integrations.find(i => i.id === integrationId);
return integration || {
id: integrationId,
name: integrationId.toUpperCase(),
icon: "/default.svg",
description: "Unknown integration"
};
};
const CatalogCard = ({ catalog, config, onChange }: {
catalog: Catalog;
config?: { enabled: boolean; showInHome: boolean };
onChange: (enabled: boolean, showInHome: boolean) => void;
}) => {
const isEnabled = config?.enabled ?? true;
const showInHome = config?.showInHome ?? true;
let integration = getIntegrationInfo(catalog.id);
if (integration.id === "streaming") {
const streamindId = catalog.id.split(".")[1];
const foundService = streamingServices.find(s => s.id === streamindId);
integration = { ...integration, icon: foundService?.icon || integration.icon };
}
const handleEnableChange = (checked: boolean) => {
onChange(checked, checked ? showInHome : false);
};
const handleShowInHomeChange = (checked: boolean) => {
onChange(isEnabled, checked);
};
return (
<Card className="h-full">
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="space-y-1">
<h1 className="font-semibold flex items-center gap-2">
<div className="w-8 h-8 flex items-center justify-center bg-gray-50 rounded-md">
<img
src={integration.icon}
alt={`${integration.name} logo`}
className="w-full h-full object-contain rounded-md"
/>
</div>
{catalog.name}
<Badge variant="outline">
{catalog.type === "movie" ? "Movie" : "Series"}
</Badge>
</h1>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground">Enable</span>
<Switch
checked={isEnabled}
onCheckedChange={handleEnableChange}
/>
<span className={`text-sm ${!isEnabled ? "text-muted-foreground/50" : "text-muted-foreground"}`}>Home</span>
<Switch
checked={showInHome}
onCheckedChange={handleShowInHomeChange}
disabled={!isEnabled}
/>
</div>
</div>
</CardHeader>
</Card>
);
};
const CatalogColumn = ({
title,
catalogs,
catalogConfigs,
onCatalogChange
}: {
title: string;
catalogs: Catalog[];
catalogConfigs: Record<string, { enabled: boolean; showInHome: boolean }>;
onCatalogChange: (catalogId: string, type: "movie" | "series", enabled: boolean, showInHome: boolean) => void;
}) => (
<div className="flex flex-col gap-6">
<h2 className="text-lg font-semibold">{title}</h2>
{catalogs.map((catalog) => (
<CatalogCard
key={`${catalog.id}-${catalog.type}`}
catalog={catalog}
config={catalogConfigs[`${catalog.id}-${catalog.type}`]}
onChange={(enabled, showInHome) => onCatalogChange(catalog.id, catalog.type, enabled, showInHome)}
/>
))}
</div>
);
const Catalogs = () => {
const { sessionId, streaming, catalogs, setCatalogs } = useConfig();
// Combina todos os catálogos que devem ser mostrados
const getAllCatalogs = () => {
let allCatalogs = [...baseCatalogs];
// Adiciona catálogos que requerem autenticação se houver sessionId
if (sessionId) {
allCatalogs = [...allCatalogs, ...authCatalogs];
}
// Adiciona catálogos de streaming selecionados
if (streaming?.length) {
const selectedStreamingCatalogs = streaming.flatMap(serviceId =>
streamingCatalogs[serviceId] || []
);
allCatalogs = [...allCatalogs, ...selectedStreamingCatalogs];
}
return allCatalogs;
};
// Converte os catálogos para um objeto de configuração
const catalogConfigs = catalogs.reduce((acc, config) => {
acc[`${config.id}-${config.type}`] = {
enabled: config.enabled,
showInHome: config.showInHome
};
return acc;
}, {} as Record<string, { enabled: boolean; showInHome: boolean }>);
const handleCatalogChange = (catalogId: string, type: "movie" | "series", enabled: boolean, showInHome: boolean) => {
setCatalogs((prev: CatalogConfig[]) => {
// Remove a configuração anterior se existir
const filtered = prev.filter(c => !(c.id === catalogId && c.type === type));
// Adiciona a nova configuração
const newConfig: CatalogConfig = { id: catalogId, type, enabled, showInHome };
return [...filtered, newConfig];
});
};
const allCatalogs = getAllCatalogs();
const movieCatalogs = allCatalogs.filter(catalog => catalog.type === "movie");
const seriesCatalogs = allCatalogs.filter(catalog => catalog.type === "series");
// Inicializa as configurações dos catálogos quando necessário
useEffect(() => {
const allCatalogIds = allCatalogs.map(c => `${c.id}-${c.type}`);
const configuredIds = catalogs.map(c => `${c.id}-${c.type}`);
// Encontra catálogos que não têm configuração
const unconfigured = allCatalogs.filter(c =>
!configuredIds.includes(`${c.id}-${c.type}`)
);
if (unconfigured.length > 0) {
// Adiciona configurações padrão para catálogos não configurados
setCatalogs((prev: CatalogConfig[]) => [
...prev,
...unconfigured.map(c => ({
id: c.id,
type: c.type,
enabled: true,
showInHome: true
} as CatalogConfig))
]);
}
}, [allCatalogs.length]);
return (
<main className="md:p-12 px-2 py-12">
<div className="flex flex-col mb-6">
<h1 className="text-xl font-semibold mb-1">Catalogs</h1>
<p className="text-gray-500 text-sm">Manage the catalogs available in the addon.</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CatalogColumn
title="Movies"
catalogs={movieCatalogs}
catalogConfigs={catalogConfigs}
onCatalogChange={handleCatalogChange}
/>
<CatalogColumn
title="TV Shows"
catalogs={seriesCatalogs}
catalogConfigs={catalogConfigs}
onCatalogChange={handleCatalogChange}
/>
</div>
</main>
);
};
export default Catalogs;
-339
View File
@@ -1,339 +0,0 @@
import { useConfig } from "@/contexts/ConfigContext";
import { KoFiDialog } from "react-kofi";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
const languages = [
{ value: "ab-AB", label: "Abkhazian" },
{ value: "aa-AA", label: "Afar" },
{ value: "af-AF", label: "Afrikaans" },
{ value: "ak-AK", label: "Akan" },
{ value: "sq-AL", label: "Albanian" },
{ value: "am-AM", label: "Amharic" },
{ value: "ar-SA", label: "Arabic (Saudi Arabia)" },
{ value: "ar-AE", label: "Arabic (UAE)" },
{ value: "an-AN", label: "Aragonese" },
{ value: "hy-HY", label: "Armenian" },
{ value: "as-AS", label: "Assamese" },
{ value: "av-AV", label: "Avaric" },
{ value: "ae-AE", label: "Avestan" },
{ value: "ay-AY", label: "Aymara" },
{ value: "az-AZ", label: "Azerbaijani" },
{ value: "bm-BM", label: "Bambara" },
{ value: "ba-BA", label: "Bashkir" },
{ value: "eu-ES", label: "Basque" },
{ value: "be-BY", label: "Belarusian" },
{ value: "bn-BD", label: "Bengali" },
{ value: "bi-BI", label: "Bislama" },
{ value: "nb-NO", label: "Bokmål" },
{ value: "bs-BS", label: "Bosnian" },
{ value: "br-BR", label: "Breton" },
{ value: "bg-BG", label: "Bulgarian" },
{ value: "my-MY", label: "Burmese" },
{ value: "cn-CN", label: "Cantonese" },
{ value: "ca-ES", label: "Catalan" },
{ value: "km-KM", label: "Central Khmer" },
{ value: "ch-GU", label: "Chamorro" },
{ value: "ce-CE", label: "Chechen" },
{ value: "ny-NY", label: "Chichewa" },
{ value: "zh-CN", label: "Chinese (China)" },
{ value: "zh-HK", label: "Chinese (Hong Kong)" },
{ value: "zh-TW", label: "Chinese (Taiwan)" },
{ value: "cu-CU", label: "Church Slavic" },
{ value: "cv-CV", label: "Chuvash" },
{ value: "kw-KW", label: "Cornish" },
{ value: "co-CO", label: "Corsican" },
{ value: "cr-CR", label: "Cree" },
{ value: "hr-HR", label: "Croatian" },
{ value: "cs-CZ", label: "Czech" },
{ value: "da-DK", label: "Danish" },
{ value: "dv-DV", label: "Divehi" },
{ value: "nl-NL", label: "Dutch" },
{ value: "dz-DZ", label: "Dzongkha" },
{ value: "en-US", label: "English (US)" },
{ value: "en-AU", label: "English (Australia)" },
{ value: "en-CA", label: "English (Canada)" },
{ value: "en-GB", label: "English (UK)" },
{ value: "en-IE", label: "English (Ireland)" },
{ value: "en-NZ", label: "English (New Zealand)" },
{ value: "eo-EO", label: "Esperanto" },
{ value: "et-EE", label: "Estonian" },
{ value: "ee-EE", label: "Ewe" },
{ value: "fo-FO", label: "Faroese" },
{ value: "fj-FJ", label: "Fijian" },
{ value: "fi-FI", label: "Finnish" },
{ value: "fr-FR", label: "French (France)" },
{ value: "fr-CA", label: "French (Canada)" },
{ value: "ff-FF", label: "Fulah" },
{ value: "gd-GD", label: "Gaelic" },
{ value: "gl-ES", label: "Galician" },
{ value: "lg-LG", label: "Ganda" },
{ value: "ka-GE", label: "Georgian" },
{ value: "de-DE", label: "German (Germany)" },
{ value: "de-AT", label: "German (Austria)" },
{ value: "de-CH", label: "German (Switzerland)" },
{ value: "el-GR", label: "Greek" },
{ value: "gn-GN", label: "Guarani" },
{ value: "gu-GU", label: "Gujarati" },
{ value: "ht-HT", label: "Haitian" },
{ value: "ha-HA", label: "Hausa" },
{ value: "he-IL", label: "Hebrew" },
{ value: "hz-HZ", label: "Herero" },
{ value: "hi-IN", label: "Hindi" },
{ value: "ho-HO", label: "Hiri Motu" },
{ value: "hu-HU", label: "Hungarian" },
{ value: "is-IS", label: "Icelandic" },
{ value: "io-IO", label: "Ido" },
{ value: "ig-IG", label: "Igbo" },
{ value: "id-ID", label: "Indonesian" },
{ value: "ia-IA", label: "Interlingua" },
{ value: "ie-IE", label: "Interlingue" },
{ value: "iu-IU", label: "Inuktitut" },
{ value: "ik-IK", label: "Inupiaq" },
{ value: "ga-GA", label: "Irish" },
{ value: "it-IT", label: "Italian" },
{ value: "ja-JP", label: "Japanese" },
{ value: "jv-JV", label: "Javanese" },
{ value: "kl-KL", label: "Kalaallisut" },
{ value: "kn-IN", label: "Kannada" },
{ value: "kr-KR", label: "Kanuri" },
{ value: "ks-KS", label: "Kashmiri" },
{ value: "kk-KZ", label: "Kazakh" },
{ value: "ki-KI", label: "Kikuyu" },
{ value: "rw-RW", label: "Kinyarwanda" },
{ value: "ky-KY", label: "Kirghiz" },
{ value: "kv-KV", label: "Komi" },
{ value: "kg-KG", label: "Kongo" },
{ value: "ko-KR", label: "Korean" },
{ value: "kj-KJ", label: "Kuanyama" },
{ value: "ku-KU", label: "Kurdish" },
{ value: "lo-LO", label: "Lao" },
{ value: "la-LA", label: "Latin" },
{ value: "lv-LV", label: "Latvian" },
{ value: "li-LI", label: "Limburgan" },
{ value: "ln-LN", label: "Lingala" },
{ value: "lt-LT", label: "Lithuanian" },
{ value: "lu-LU", label: "Luba-Katanga" },
{ value: "lb-LB", label: "Luxembourgish" },
{ value: "mk-MK", label: "Macedonian" },
{ value: "mg-MG", label: "Malagasy" },
{ value: "ms-MY", label: "Malay (Malaysia)" },
{ value: "ms-SG", label: "Malay (Singapore)" },
{ value: "ml-IN", label: "Malayalam" },
{ value: "mt-MT", label: "Maltese" },
{ value: "gv-GV", label: "Manx" },
{ value: "mi-MI", label: "Maori" },
{ value: "mr-MR", label: "Marathi" },
{ value: "mh-MH", label: "Marshallese" },
{ value: "mo-MO", label: "Moldavian" },
{ value: "mn-MN", label: "Mongolian" },
{ value: "na-NA", label: "Nauru" },
{ value: "nv-NV", label: "Navajo" },
{ value: "nd-ND", label: "North Ndebele" },
{ value: "nr-NR", label: "South Ndebele" },
{ value: "ng-NG", label: "Ndonga" },
{ value: "ne-NE", label: "Nepali" },
{ value: "se-SE", label: "Northern Sami" },
{ value: "no-NO", label: "Norwegian" },
{ value: "nn-NN", label: "Norwegian Nynorsk" },
{ value: "oc-OC", label: "Occitan" },
{ value: "oj-OJ", label: "Ojibwa" },
{ value: "or-OR", label: "Oriya" },
{ value: "om-OM", label: "Oromo" },
{ value: "os-OS", label: "Ossetian" },
{ value: "pi-PI", label: "Pali" },
{ value: "pa-PA", label: "Panjabi" },
{ value: "fa-IR", label: "Persian" },
{ value: "pl-PL", label: "Polish" },
{ value: "pt-PT", label: "Portuguese (Portugal)" },
{ value: "pt-BR", label: "Portuguese (Brazil)" },
{ value: "ps-PS", label: "Pushto" },
{ value: "qu-QU", label: "Quechua" },
{ value: "ro-RO", label: "Romanian" },
{ value: "rm-RM", label: "Romansh" },
{ value: "rn-RN", label: "Rundi" },
{ value: "ru-RU", label: "Russian" },
{ value: "sm-SM", label: "Samoan" },
{ value: "sg-SG", label: "Sango" },
{ value: "sa-SA", label: "Sanskrit" },
{ value: "sc-SC", label: "Sardinian" },
{ value: "sr-RS", label: "Serbian" },
{ value: "sh-SH", label: "Serbo-Croatian" },
{ value: "sn-SN", label: "Shona" },
{ value: "ii-II", label: "Sichuan Yi" },
{ value: "sd-SD", label: "Sindhi" },
{ value: "si-LK", label: "Sinhala" },
{ value: "sk-SK", label: "Slovak" },
{ value: "sl-SI", label: "Slovenian" },
{ value: "so-SO", label: "Somali" },
{ value: "st-ST", label: "Sotho" },
{ value: "es-ES", label: "Spanish (Spain)" },
{ value: "es-MX", label: "Spanish (Mexico)" },
{ value: "su-SU", label: "Sundanese" },
{ value: "sw-SW", label: "Swahili" },
{ value: "ss-SS", label: "Swati" },
{ value: "sv-SE", label: "Swedish" },
{ value: "tl-PH", label: "Tagalog" },
{ value: "ty-TY", label: "Tahitian" },
{ value: "tg-TG", label: "Tajik" },
{ value: "ta-IN", label: "Tamil" },
{ value: "tt-TT", label: "Tatar" },
{ value: "te-IN", label: "Telugu" },
{ value: "th-TH", label: "Thai" },
{ value: "bo-BO", label: "Tibetan" },
{ value: "ti-TI", label: "Tigrinya" },
{ value: "to-TO", label: "Tonga" },
{ value: "ts-TS", label: "Tsonga" },
{ value: "tn-TN", label: "Tswana" },
{ value: "tr-TR", label: "Turkish" },
{ value: "tk-TK", label: "Turkmen" },
{ value: "tw-TW", label: "Twi" },
{ value: "ug-UG", label: "Uighur" },
{ value: "uk-UA", label: "Ukrainian" },
{ value: "ur-UR", label: "Urdu" },
{ value: "uz-UZ", label: "Uzbek" },
{ value: "ve-VE", label: "Venda" },
{ value: "vi-VN", label: "Vietnamese" },
{ value: "vo-VO", label: "Volapük" },
{ value: "wa-WA", label: "Walloon" },
{ value: "cy-CY", label: "Welsh" },
{ value: "fy-FY", label: "Western Frisian" },
{ value: "wo-WO", label: "Wolof" },
{ value: "xh-XH", label: "Xhosa" },
{ value: "yi-YI", label: "Yiddish" },
{ value: "yo-YO", label: "Yoruba" },
{ value: "za-ZA", label: "Zhuang" },
{ value: "zu-ZA", label: "Zulu" }
];
interface Movie {
imdb_id: string;
name: string;
description: string;
background: string;
}
export default function Home() {
const { language, setLanguage } = useConfig();
const [backgroundUrl, setBackgroundUrl] = useState("");
useEffect(() => {
const fetchPopularMovies = async () => {
try {
const response = await fetch('https://cinemeta-catalogs.strem.io/top/catalog/movie/top.json');
const data = await response.json();
const moviesWithId = data.metas.filter(movie => movie.imdb_id);
if (moviesWithId.length > 0) {
const randomIndex = Math.floor(Math.random() * moviesWithId.length);
const randomMovie = moviesWithId[randomIndex];
const highQualityImageUrl = `https://images.metahub.space/background/medium/${randomMovie.imdb_id}/img`;
setBackgroundUrl(highQualityImageUrl);
}
} catch (error) {
console.error('Error fetching popular movies:', error);
setBackgroundUrl('https://images.metahub.space/background/medium/tt0816692/img');
}
};
fetchPopularMovies();
}, []);
return (
<div className="relative min-h-screen overflow-hidden">
<div className="fixed inset-0 z-0">
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/50 to-black/80 z-10" />
<div
className="absolute inset-0 blur-sm"
style={{
backgroundImage: `url(${backgroundUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
</div>
<div className="relative z-20 container mx-auto px-4 py-12 min-h-screen flex flex-col items-center justify-center text-white">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center space-y-8 max-w-3xl"
>
<img
src="https://www.themoviedb.org/assets/2/v4/logos/v2/blue_short-8e7b30f73a4020692ccca9c88bafe5dcb6f8a62a4c6bc55cd9ba82bb2cd95f6c.svg"
alt="TMDB Logo"
className="w-64 mx-auto mb-8"
/>
<h1 className="text-4xl sm:text-5xl font-bold mb-4">
The Movie Database Addon
</h1>
<p className="text-xl sm:text-2xl text-gray-300 mb-8">
Explore a vast catalog of movies and TV shows with metadata provided by TMDB.
Version 3.1.2
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8">
<div className="w-full sm:w-64">
<Select value={language} onValueChange={setLanguage} defaultValue="en-US">
<SelectTrigger className="bg-white/10 border-white/20 text-white">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent className="bg-background/95 backdrop-blur-sm">
{languages.map((lang) => (
<SelectItem
key={lang.value}
value={lang.value}
className="cursor-pointer"
>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<KoFiDialog
color="#01b4e4"
textColor="#fff"
id="mrcanelas"
label="Support me"
padding={6}
iframe={false}
buttonRadius="6px"
className="w-full sm:w-auto"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 text-left">
<div className="bg-white/10 rounded-lg p-6 backdrop-blur-sm">
<h3 className="text-xl font-semibold mb-2">Movies</h3>
<p className="text-gray-300">
Access detailed information about thousands of movies, including synopses,
cast, ratings, and much more.
</p>
</div>
<div className="bg-white/10 rounded-lg p-6 backdrop-blur-sm">
<h3 className="text-xl font-semibold mb-2">TV Shows</h3>
<p className="text-gray-300">
Explore TV series, seasons, episodes, and stay up to date
with your favorite shows.
</p>
</div>
</div>
</motion.div>
</div>
</div>
);
}
-22
View File
@@ -1,22 +0,0 @@
import { IntegrationCard } from "@/components/IntegrationCard";
import { integrations } from "@/data/integrations";
const Integrations = () => {
return (
<main className="px-12 py-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{integrations.map((integration) => (
<IntegrationCard
key={integration.id}
id={integration.id}
name={integration.name}
icon={integration.icon}
description={integration.description}
/>
))}
</div>
</main>
);
};
export default Integrations;
-27
View File
@@ -1,27 +0,0 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error(
"404 Error: User attempted to access non-existent route:",
location.pathname
);
}, [location.pathname]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="text-xl text-gray-600 mb-4">Oops! Page not found</p>
<a href="/" className="text-blue-500 hover:text-blue-700 underline">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;
-64
View File
@@ -1,64 +0,0 @@
import { Card } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { useConfig } from "@/contexts/ConfigContext";
const Others = () => {
const { includeAdult, setIncludeAdult } = useConfig();
const { provideImdbId, setProvideImdbId } = useConfig();
const { tmdbPrefix, setTmdbPrefix } = useConfig();
return (
<main className="md:p-12 px-2 py-12">
<div className="flex flex-col mb-6">
<h1 className="text-xl font-semibold mb-1">Addon Settings</h1>
<p className="text-gray-500 text-sm">
Customize the addon settings to suit your needs.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card className="flex flex-row items-center justify-between p-4 sm:p-6 hover:shadow-lg transition-shadow cursor-pointer">
<div className="space-y-0.5">
<h1 className="text-sm font-semibold mb-1">Enable adult content</h1>
<p className="text-gray-500 text-sm">
Include adult content in search results.
</p>
</div>
<Switch
checked={includeAdult}
onCheckedChange={() => setIncludeAdult(!includeAdult)}
/>
</Card>
<Card className="flex flex-row items-center justify-between p-4 sm:p-6 hover:shadow-lg transition-shadow cursor-pointer">
<div className="space-y-0.5">
<h1 className="text-sm font-semibold mb-1">
Provide IMDB metadata
</h1>
<p className="text-gray-500 text-sm">
Include IMDB IDs in metadata for better integration with other addons.
</p>
</div>
<Switch
checked={provideImdbId}
onCheckedChange={() => setProvideImdbId(!provideImdbId)}
/>
</Card>
<Card className="flex flex-row items-center justify-between p-4 sm:p-6 hover:shadow-lg transition-shadow cursor-pointer">
<div className="space-y-0.5">
<h1 className="text-sm font-semibold mb-1">
Use TMDB prefix
</h1>
<p className="text-gray-500 text-sm">
Add "TMDB -" prefix to all catalog names for better organization.
</p>
</div>
<Switch
checked={tmdbPrefix}
onCheckedChange={() => setTmdbPrefix(!tmdbPrefix)}
/>
</Card>
</div>
</main>
);
};
export default Others;
-4
View File
@@ -1,4 +0,0 @@
/* Estilo para o dialog do Ko-fi */
[role="dialog"]:has(#kofiframe) {
height: 95vh !important;
}
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
-29
View File
@@ -1,29 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-unused-vars": "off",
},
}
);

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

-18
View File
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Movie Database Addon - Stremio Addon</title>
<meta name="description" content="Lovable Generated Project" />
<meta name="author" content="Lovable" />
<meta property="og:image" content="/og-image.png" />
<link rel="shortcut icon" href="https://github.com/mrcanelas/tmdb-addon/raw/main/images/favicon.png" type="image/x-icon" />
</head>
<body>
<div id="root"></div>
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
<script type="module" src="/configure/src/main.tsx"></script>
</body>
</html>
+4 -4
View File
@@ -42,7 +42,7 @@ const respond = function (res, data, opts) {
res.send(data);
};
addon.get("/", function (_, res) {
addon.get("/", async function (_, res) {
res.redirect("/configure");
});
@@ -57,10 +57,10 @@ addon.get("/session_id", async function (req, res) {
respond(res, sessionId);
});
addon.use(express.static('../dist'));
addon.use('/configure', express.static(path.join(__dirname, 'configure/dist')));
addon.get("/:catalogChoices?/configure", function (req, res) {
res.sendFile(path.join(__dirname, '../dist/index.html'));
addon.get("/:catalogChoices?/configure", async function (req, res) {
res.sendFile(path.join(__dirname + "/configure.html"));
});
addon.get("/:catalogChoices?/manifest.json", async function (req, res) {
@@ -1,7 +1,7 @@
require("dotenv").config();
const { getGenreList } = require("./getGenreList");
const { getLanguages } = require("./getLanguages");
const packageJson = require("../../package.json");
const package = require("../package.json");
const catalogsTranslations = require("../static/translations.json");
const CATALOG_TYPES = require("../static/catalog-types.json");
const DEFAULT_LANGUAGE = "en-US";
@@ -154,13 +154,13 @@ async function getManifest(config) {
const descriptionSuffix = language && language !== DEFAULT_LANGUAGE ? ` with ${language} language.` : ".";
return {
id: packageJson.name,
version: packageJson.version,
id: package.name,
version: package.version,
favicon: "https://github.com/mrcanelas/tmdb-addon/raw/main/images/favicon.png",
logo: "https://github.com/mrcanelas/tmdb-addon/raw/main/images/logo.png",
background: "https://github.com/mrcanelas/tmdb-addon/raw/main/images/background.png",
name: "The Movie Database Addon",
description: packageJson.description + descriptionSuffix,
description: package.description + descriptionSuffix,
resources: ["catalog", "meta"],
types: ["movie", "series"],
idPrefixes: provideImdbId ? ["tmdb:", "tt"] : ["tmdb:"],
+5
View File
@@ -0,0 +1,5 @@
{
"version": 2,
"builds": [{ "src": "server.js", "use": "@now/node" }],
"routes": [{ "src": "/.*", "dest": "/server.js" }]
}
+2017 -6128
View File
File diff suppressed because it is too large Load Diff
+17 -58
View File
@@ -1,7 +1,22 @@
{
"name": "tmdb-addon",
"version": "3.1.2",
"version": "3.1.1",
"description": "Metadata provided by TMDB",
"main": "server.js",
"dependencies": {
"cache-manager": "^3.6.3",
"cache-manager-mongodb": "^0.3.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"fanart.tv-api": "^2.0.1",
"moviedb-promise": "^4.0.7",
"transliteration": "^2.3.5",
"url-exists": "^1.0.3"
},
"scripts": {
"dev": "nodemon server.js",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mrcanelas/tmdb-addon"
@@ -17,63 +32,7 @@
"url": "https://github.com/mrcanelas/tmdb-addon/issues"
},
"homepage": "https://github.com/mrcanelas/tmdb-addon#readme",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview",
"start": "node addon/server.js"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.56.2",
"axios": "^1.7.9",
"cache-manager": "^3.6.3",
"cache-manager-mongodb": "^0.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"fanart.tv-api": "^2.0.1",
"framer-motion": "^12.0.6",
"lucide-react": "^0.462.0",
"moviedb-promise": "^4.0.7",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-kofi": "^0.0.2",
"react-router-dom": "^6.26.2",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"transliteration": "^2.3.5",
"url-exists": "^1.0.3"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"lovable-tagger": "^1.0.19",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
"nodemon": "^3.1.4"
}
}

Some files were not shown because too many files have changed in this diff Show More