mirror of
https://github.com/Viren070/tmdb-addon.git
synced 2025-12-01 23:18:11 +01:00
feat: add age rating filter system
- Add age rating selection component with visual badges - Implement US certification filtering for movies and TV shows - Add certification parameters to TMDB API calls - Create age rating data structure with G, PG, PG-13, R, NC-17 ratings - Add info message about trending catalogs limitation - Handle certification filtering in discover and search endpoints This feature allows users to filter content based on US age ratings, helping them find appropriate content for their desired audience.
This commit is contained in:
+2
-3
@@ -82,7 +82,6 @@ addon.get("/:catalogChoices?/catalog/:type/:id/:extra?.json", async function (re
|
||||
const { catalogChoices, type, id } = req.params;
|
||||
const config = parseConfig(catalogChoices)
|
||||
const language = config.language || DEFAULT_LANGUAGE;
|
||||
const includeAdult = config.includeAdult || false
|
||||
const rpdbkey = config.rpdbkey
|
||||
const sessionId = config.sessionId
|
||||
const { genre, skip, search } = req.params.extra
|
||||
@@ -96,7 +95,7 @@ addon.get("/:catalogChoices?/catalog/:type/:id/:extra?.json", async function (re
|
||||
const args = [type, language, page];
|
||||
|
||||
if (search) {
|
||||
metas = await getSearch(type, language, search, includeAdult);
|
||||
metas = await getSearch(type, language, search, config);
|
||||
} else {
|
||||
switch (id) {
|
||||
case "tmdb.trending":
|
||||
@@ -109,7 +108,7 @@ addon.get("/:catalogChoices?/catalog/:type/:id/:extra?.json", async function (re
|
||||
metas = await getWatchList(...args, sessionId);
|
||||
break;
|
||||
default:
|
||||
metas = await getCatalog(...args, id, genre);
|
||||
metas = await getCatalog(...args, id, genre, config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -7,11 +7,9 @@ const { getTrending } = require("./getTrending");
|
||||
const { parseMedia } = require("../utils/parseProps");
|
||||
const CATALOG_TYPES = require("../static/catalog-types.json");
|
||||
|
||||
async function getCatalog(type, language, page, id, genre) {
|
||||
if (id === "tmdb.top" && !genre) return await getTrending(type, language, page, "week");
|
||||
|
||||
async function getCatalog(type, language, page, id, genre, config) {
|
||||
const genreList = await getGenreList(language, type);
|
||||
const parameters = await buildParameters(type, language, page, id, genre, genreList);
|
||||
const parameters = await buildParameters(type, language, page, id, genre, genreList, config);
|
||||
|
||||
const fetchFunction = type === "movie" ? moviedb.discoverMovie.bind(moviedb) : moviedb.discoverTv.bind(moviedb);
|
||||
|
||||
@@ -22,9 +20,32 @@ async function getCatalog(type, language, page, id, genre) {
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
async function buildParameters(type, language, page, id, genre, genreList) {
|
||||
async function buildParameters(type, language, page, id, genre, genreList, config) {
|
||||
const languages = await getLanguages();
|
||||
const parameters = { language, page, 'vote_count.gte': 10 };
|
||||
const parameters = { language, page };
|
||||
|
||||
if (config.ageRating) {
|
||||
switch(config.ageRating) {
|
||||
case "G":
|
||||
parameters.certification_country = "US";
|
||||
parameters.certification = type === "movie" ? "G" : "TV-G";
|
||||
break;
|
||||
case "PG":
|
||||
parameters.certification_country = "US";
|
||||
parameters.certification = type === "movie" ? ["G", "PG"].join("|") : ["TV-G", "TV-PG"].join("|");
|
||||
break;
|
||||
case "PG-13":
|
||||
parameters.certification_country = "US";
|
||||
parameters.certification = type === "movie" ? ["G", "PG", "PG-13"].join("|") : ["TV-G", "TV-PG", "TV-14"].join("|");
|
||||
break;
|
||||
case "R":
|
||||
parameters.certification_country = "US";
|
||||
parameters.certification = type === "movie" ? ["G", "PG", "PG-13", "R"].join("|") : ["TV-G", "TV-PG", "TV-14", "TV-MA"].join("|");
|
||||
break;
|
||||
case "NC-17":
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (id.includes("streaming")) {
|
||||
const provider = findProvider(id.split(".")[1]);
|
||||
|
||||
+29
-3
@@ -8,18 +8,44 @@ function isNonLatin(text) {
|
||||
return /[^\u0000-\u007F]/.test(text);
|
||||
}
|
||||
|
||||
async function getSearch(type, language, query, include_adult) {
|
||||
async function getSearch(type, language, query, config) {
|
||||
let searchQuery = query;
|
||||
|
||||
if (isNonLatin(query)) {
|
||||
searchQuery = transliterate(query);
|
||||
}
|
||||
|
||||
const parameters = {
|
||||
query: searchQuery,
|
||||
language,
|
||||
include_adult: config.includeAdult
|
||||
};
|
||||
|
||||
// Adicionar filtro de classificação etária
|
||||
if (config.ageRating) {
|
||||
parameters.certification_country = "US";
|
||||
switch(config.ageRating) {
|
||||
case "G":
|
||||
parameters.certification = type === "movie" ? "G" : "TV-G";
|
||||
break;
|
||||
case "PG":
|
||||
parameters.certification = type === "movie" ? ["G", "PG"].join("|") : ["TV-G", "TV-PG"].join("|");
|
||||
break;
|
||||
case "PG-13":
|
||||
parameters.certification = type === "movie" ? ["G", "PG", "PG-13"].join("|") : ["TV-G", "TV-PG", "TV-14"].join("|");
|
||||
break;
|
||||
case "R":
|
||||
parameters.certification = type === "movie" ? ["G", "PG", "PG-13", "R"].join("|") : ["TV-G", "TV-PG", "TV-14", "TV-MA"].join("|");
|
||||
break;
|
||||
// NC-17 não tem filtro (mostra tudo)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "movie") {
|
||||
const searchMovie = [];
|
||||
|
||||
await moviedb
|
||||
.searchMovie({ query, language, include_adult })
|
||||
.searchMovie(parameters)
|
||||
.then((res) => {
|
||||
res.results.map((el) => {searchMovie.push(parseMedia(el, 'movie'));});
|
||||
})
|
||||
@@ -61,7 +87,7 @@ async function getSearch(type, language, query, include_adult) {
|
||||
const searchTv = [];
|
||||
|
||||
await moviedb
|
||||
.searchTv({ query, language, include_adult })
|
||||
.searchTv(parameters)
|
||||
.then((res) => {
|
||||
res.results.map((el) => {searchTv.push(parseMedia(el, 'tv'))});
|
||||
})
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ageRatings } from "@/data/ageRatings";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
export function AgeRatingSelect() {
|
||||
const { ageRating, setAgeRating } = useConfig();
|
||||
|
||||
const selectedRating = ageRatings.find(rating => rating.id === ageRating);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setAgeRating(value === "NONE" ? undefined : value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Age Rating</label>
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
<Info className="h-3 w-3 mr-1" />
|
||||
Not available for trending catalogs
|
||||
</div>
|
||||
</div>
|
||||
<Select value={ageRating || "NONE"} onValueChange={handleChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue>
|
||||
{selectedRating && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={`${selectedRating.badge.color} text-white w-16 justify-center`}>
|
||||
{selectedRating.badge.text}
|
||||
</Badge>
|
||||
<span>{selectedRating.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background border shadow-md">
|
||||
{ageRatings.map((rating) => (
|
||||
<SelectItem key={rating.id} value={rating.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={`${rating.badge.color} text-white w-16 justify-center`}>
|
||||
{rating.badge.text}
|
||||
</Badge>
|
||||
<div className="flex flex-col">
|
||||
<span>{rating.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export function MultiActionButton() {
|
||||
tmdbPrefix,
|
||||
language,
|
||||
sessionId,
|
||||
ageRating,
|
||||
catalogs
|
||||
} = useConfig();
|
||||
const [currentAction, setCurrentAction] = useState<number>(0);
|
||||
@@ -33,6 +34,7 @@ export function MultiActionButton() {
|
||||
tmdbPrefix,
|
||||
language,
|
||||
sessionId,
|
||||
ageRating,
|
||||
catalogs: catalogs.map(catalog => ({
|
||||
...catalog,
|
||||
enabled: true
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
@@ -25,6 +25,7 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [streaming, setStreaming] = useState<string[]>([]);
|
||||
const [catalogs, setCatalogs] = useState<CatalogConfig[]>([]);
|
||||
const [ageRating, setAgeRating] = useState<string | undefined>(undefined);
|
||||
|
||||
const loadConfigFromUrl = () => {
|
||||
try {
|
||||
@@ -80,6 +81,7 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
||||
sessionId,
|
||||
streaming,
|
||||
catalogs,
|
||||
ageRating,
|
||||
setRpdbkey,
|
||||
setMdblistkey,
|
||||
setIncludeAdult,
|
||||
@@ -89,6 +91,7 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
||||
setSessionId,
|
||||
setStreaming,
|
||||
setCatalogs,
|
||||
setAgeRating,
|
||||
loadConfigFromUrl
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ConfigContextType {
|
||||
sessionId: string;
|
||||
streaming: string[];
|
||||
catalogs: CatalogConfig[];
|
||||
ageRating: string;
|
||||
setRpdbkey: (value: string) => void;
|
||||
setMdblistkey: (value: string) => void;
|
||||
setIncludeAdult: (value: boolean) => void;
|
||||
@@ -26,6 +27,7 @@ export interface ConfigContextType {
|
||||
setSessionId: (value: string) => void;
|
||||
setStreaming: (value: string[]) => void;
|
||||
setCatalogs: (value: CatalogConfig[] | ((prev: CatalogConfig[]) => CatalogConfig[])) => void;
|
||||
setAgeRating: (value: string) => void;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||
@@ -0,0 +1,76 @@
|
||||
export interface AgeRating {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
order: number;
|
||||
badge: {
|
||||
text: string;
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ageRatings: AgeRating[] = [
|
||||
{
|
||||
id: "NONE",
|
||||
name: "No Restriction",
|
||||
description: "Show all content without age restrictions",
|
||||
order: 0,
|
||||
badge: {
|
||||
text: "ALL",
|
||||
color: "bg-gray-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "G",
|
||||
name: "General Audience",
|
||||
description: "All ages admitted. There is no content that would be objectionable to most parents.",
|
||||
order: 1,
|
||||
badge: {
|
||||
text: "G",
|
||||
color: "bg-green-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "PG",
|
||||
name: "Parental Guidance",
|
||||
description: "Some material may not be suitable for children under 10. May contain mild language, crude/suggestive humor, scary moments and/or violence.",
|
||||
order: 2,
|
||||
badge: {
|
||||
text: "PG",
|
||||
color: "bg-blue-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "PG-13",
|
||||
name: "Parental Guidance 13",
|
||||
description: "Some material may be inappropriate for children under 13. May contain sexual content, brief nudity, strong language, mature themes and intense action violence.",
|
||||
order: 3,
|
||||
badge: {
|
||||
text: "PG-13",
|
||||
color: "bg-yellow-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "R",
|
||||
name: "Restricted",
|
||||
description: "Under 17 requires accompanying parent or adult guardian. May contain strong profanity, graphic sexuality, nudity, strong violence, horror, gore, and drug use.",
|
||||
order: 4,
|
||||
badge: {
|
||||
text: "R",
|
||||
color: "bg-red-500"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "NC-17",
|
||||
name: "Adults Only",
|
||||
description: "Adults only. Contains excessive graphic violence, intense sex, depraved behavior, explicit drug abuse or any other elements beyond R rating.",
|
||||
order: 5,
|
||||
badge: {
|
||||
text: "NC-17",
|
||||
color: "bg-purple-500"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Ordenar por ordem
|
||||
ageRatings.sort((a, b) => a.order - b.order);
|
||||
@@ -6,6 +6,7 @@ interface AddonConfig {
|
||||
tmdbPrefix?: boolean;
|
||||
language?: string;
|
||||
sessionId?: string;
|
||||
ageRating?: string;
|
||||
catalogs?: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -15,20 +16,16 @@ interface AddonConfig {
|
||||
}
|
||||
|
||||
export function generateAddonUrl(config: AddonConfig): string {
|
||||
// Criar um novo objeto apenas com os valores necessários
|
||||
const configToEncode = {
|
||||
...config,
|
||||
// Remove os itens se forem nulos/vazios
|
||||
rpdbkey: config.rpdbkey || undefined,
|
||||
mdblistkey: config.mdblistkey || 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import { AgeRatingSelect } from "@/components/AgeRatingSelect";
|
||||
|
||||
const Others = () => {
|
||||
const { includeAdult, setIncludeAdult } = useConfig();
|
||||
@@ -16,16 +17,19 @@ const Others = () => {
|
||||
</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">
|
||||
<Card className="p-6">
|
||||
<AgeRatingSelect />
|
||||
</Card>
|
||||
<Card className="flex flex-row items-center justify-between p-6">
|
||||
<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.
|
||||
<h2 className="text-sm font-medium">Enable adult content</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Include adult content in search results
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={includeAdult}
|
||||
onCheckedChange={() => setIncludeAdult(!includeAdult)}
|
||||
onCheckedChange={setIncludeAdult}
|
||||
/>
|
||||
</Card>
|
||||
<Card className="flex flex-row items-center justify-between p-4 sm:p-6 hover:shadow-lg transition-shadow cursor-pointer">
|
||||
|
||||
Vendored
+1
@@ -1 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="react" />
|
||||
|
||||
Generated
+1134
-19
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -29,9 +29,10 @@
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
@@ -44,6 +45,7 @@
|
||||
"cache-manager-mongodb": "^0.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.2",
|
||||
"fanart.tv-api": "^1.0.3",
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./configure/src/*"]
|
||||
}
|
||||
},
|
||||
"types": ["vite/client", "react", "react-dom"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["configure/src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user