From c7825f20b91ec09e8cb72a5407b44d1108736250 Mon Sep 17 00:00:00 2001 From: Foxix Date: Thu, 17 Jul 2025 22:03:56 +0300 Subject: [PATCH] Release 2.3.2 --- src/app/layout.tsx | 37 ++++++ src/app/login/page.tsx | 22 ++-- src/app/movie/[id]/MovieContent.tsx | 5 + src/app/page.tsx | 10 +- src/app/tv/[id]/TVContent.tsx | 6 + src/components/TorrentSelector.tsx | 197 ++++++++++++++++++++++++++++ src/components/ui/button.tsx | 56 ++++++++ src/lib/utils.ts | 7 + 8 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 src/components/TorrentSelector.tsx create mode 100644 src/components/ui/button.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7f7ff5..f88d36e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import { Inter } from 'next/font/google'; +import Script from 'next/script'; import './globals.css'; import { ClientLayout } from '@/components/ClientLayout'; import { Providers } from '@/components/Providers'; @@ -22,8 +23,44 @@ export default function RootLayout({ + + {/* Google tag (gtag.js) */} + + + {/* Google Tag Manager */} + + {/* End Google Tag Manager */} + {/* Google Tag Manager (noscript) */} + + {/* End Google Tag Manager (noscript) */} {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 223f73e..e00dc59 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,11 +1,11 @@ -'use client'; - -import dynamic from 'next/dynamic'; - -const LoginClient = dynamic(() => import('./LoginClient'), { - ssr: false -}); - -export default function LoginPage() { - return ; -} +'use client'; + +import dynamic from 'next/dynamic'; + +const LoginClient = dynamic(() => import('./LoginClient'), { + ssr: false +}); + +export default function LoginPage() { + return ; +} diff --git a/src/app/movie/[id]/MovieContent.tsx b/src/app/movie/[id]/MovieContent.tsx index 57f40ee..5097405 100644 --- a/src/app/movie/[id]/MovieContent.tsx +++ b/src/app/movie/[id]/MovieContent.tsx @@ -6,6 +6,7 @@ import { moviesAPI } from '@/lib/neoApi'; import { getImageUrl } from '@/lib/neoApi'; import type { MovieDetails } from '@/lib/api'; import MoviePlayer from '@/components/MoviePlayer'; +import TorrentSelector from '@/components/TorrentSelector'; import FavoriteButton from '@/components/FavoriteButton'; import Reactions from '@/components/Reactions'; import { formatDate } from '@/lib/utils'; @@ -142,6 +143,10 @@ export default function MovieContent({ movieId, initialMovie }: MovieContentProp poster={movie.poster_path || ''} imdbId={imdbId} /> + )} diff --git a/src/app/page.tsx b/src/app/page.tsx index 01ee0a4..ecf41df 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -81,11 +81,11 @@ export default function HomePage() {
{activeTab === 'popular' && ( - + )}
diff --git a/src/app/tv/[id]/TVContent.tsx b/src/app/tv/[id]/TVContent.tsx index dbd0af5..b61b02f 100644 --- a/src/app/tv/[id]/TVContent.tsx +++ b/src/app/tv/[id]/TVContent.tsx @@ -6,6 +6,7 @@ import { tvAPI } from '@/lib/api'; import { getImageUrl } from '@/lib/neoApi'; import type { TVShowDetails } from '@/lib/api'; import MoviePlayer from '@/components/MoviePlayer'; +import TorrentSelector from '@/components/TorrentSelector'; import FavoriteButton from '@/components/FavoriteButton'; import Reactions from '@/components/Reactions'; import { formatDate } from '@/lib/utils'; @@ -145,6 +146,11 @@ export default function TVContent({ showId, initialShow }: TVContentProps) { poster={show.poster_path || ''} imdbId={imdbId} /> + )} diff --git a/src/components/TorrentSelector.tsx b/src/components/TorrentSelector.tsx new file mode 100644 index 0000000..8fb8399 --- /dev/null +++ b/src/components/TorrentSelector.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Loader2, AlertTriangle, Copy, Check } from 'lucide-react'; + +interface Torrent { + magnet: string; + title?: string; + name?: string; + quality?: string; + seeders?: number; + size_gb?: number; +} + +interface GroupedTorrents { + [quality: string]: Torrent[]; +} + +interface SeasonGroupedTorrents { + [season: string]: GroupedTorrents; +} + +interface TorrentSelectorProps { + imdbId: string | null; + type: 'movie' | 'tv'; + totalSeasons?: number; +} + +export default function TorrentSelector({ imdbId, type, totalSeasons }: TorrentSelectorProps) { + const [torrents, setTorrents] = useState(null); + const [selectedSeason, setSelectedSeason] = useState(type === 'movie' ? 1 : null); + const [selectedMagnet, setSelectedMagnet] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isCopied, setIsCopied] = useState(false); + + // Для TV показов автоматически выбираем первый сезон + useEffect(() => { + if (type === 'tv' && totalSeasons && totalSeasons > 0 && !selectedSeason) { + setSelectedSeason(1); + } + }, [type, totalSeasons, selectedSeason]); + + useEffect(() => { + if (!imdbId) return; + + // Для фильмов загружаем сразу + if (type === 'movie') { + fetchTorrents(); + } + // Для TV показов загружаем только когда выбран сезон + else if (type === 'tv' && selectedSeason) { + fetchTorrents(); + } + }, [imdbId, type, selectedSeason]); + + const fetchTorrents = async () => { + setLoading(true); + setError(null); + setSelectedMagnet(null); + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + if (!apiUrl) { + throw new Error('API URL не настроен'); + } + + let url = `${apiUrl}/torrents/search/${imdbId}?type=${type}`; + + if (type === 'tv' && selectedSeason) { + url += `&season=${selectedSeason}`; + } + + console.log('API URL:', url, 'IMDB:', imdbId, 'season:', selectedSeason); + const res = await fetch(url); + if (!res.ok) { + throw new Error('Failed to fetch torrents'); + } + const data = await res.json(); + if (data.total === 0) { + setError('Торренты не найдены.'); + } else { + setTorrents(data.results as Torrent[]); + } + } catch (err) { + console.error(err); + setError('Не удалось загрузить список торрентов.'); + } finally { + setLoading(false); + } + }; + + const handleQualitySelect = (torrent: Torrent) => { + setSelectedMagnet(torrent.magnet); + setIsCopied(false); + }; + + const handleCopy = () => { + if (!selectedMagnet) return; + navigator.clipboard.writeText(selectedMagnet); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }; + + if (loading) { + return ( +
+ + Загрузка торрентов... +
+ ); + } + + if (error) { + return ( +
+ + {error} +
+ ); + } + + if (!torrents) return null; + + const renderTorrentButtons = (list: Torrent[]) => { + if (!list?.length) { + return ( +

+ Торрентов для выбранного сезона нет. +

+ ); + } + + return list.map(torrent => { + const size = torrent.size_gb; + const label = torrent.title || torrent.name || 'Раздача'; + + return ( + + ); + }); + }; + + return ( +
+ {type === 'tv' && totalSeasons && totalSeasons > 0 && ( +
+

Сезоны

+
+ {Array.from({ length: totalSeasons }, (_, i) => i + 1).map(season => ( + + ))} +
+
+ )} + + {selectedSeason && torrents && ( +
+

Раздачи

+
+ {renderTorrentButtons(torrents)} +
+
+ )} + + {selectedMagnet && ( +
+

Magnet-ссылка

+
+
+ {selectedMagnet} +
+ +
+
+ )} +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..ec5433c --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +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 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", + { + 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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8be6bcc..d23ff61 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,3 +1,10 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + export const generateVerificationToken = () => { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); };