From ede36131122bf92567dcaf156154e77de398b4f6 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:37:41 -0600 Subject: [PATCH] clean up details modal --- src/assets/locales/en.json | 3 +- .../{DetailsHeader.tsx => DetailsBody.tsx} | 32 +- .../overlays/details/DetailsContent.tsx | 4 +- .../overlays/details/DetailsRatings.tsx | 39 - .../overlays/details/old.DetailsModal.txt | 1119 ----------------- src/components/overlays/details/types.ts | 3 +- 6 files changed, 23 insertions(+), 1177 deletions(-) rename src/components/overlays/details/{DetailsHeader.tsx => DetailsBody.tsx} (90%) delete mode 100644 src/components/overlays/details/old.DetailsModal.txt diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index f1b4a439..4dc35467 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -212,7 +212,8 @@ "season": "Season", "episode": "Episode", "airs": "Airs", - "endsAt": "Ends at {{time}}" + "endsAt": "Ends at {{time}}", + "trailer": "Trailer" }, "migration": { "loginRequired": "You must be logged in to migrate your data! Please go back and login to continue.", diff --git a/src/components/overlays/details/DetailsHeader.tsx b/src/components/overlays/details/DetailsBody.tsx similarity index 90% rename from src/components/overlays/details/DetailsHeader.tsx rename to src/components/overlays/details/DetailsBody.tsx index 6e8eaf3b..b80e1231 100644 --- a/src/components/overlays/details/DetailsHeader.tsx +++ b/src/components/overlays/details/DetailsBody.tsx @@ -11,9 +11,9 @@ import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; import { MediaBookmarkButton } from "@/components/media/MediaBookmark"; -import { DetailsHeaderProps } from "./types"; +import { DetailsBodyProps } from "./types"; -export function DetailsHeader({ +export function DetailsBody({ data, onPlayClick, onTrailerClick, @@ -24,7 +24,7 @@ export function DetailsHeader({ releaseDate, seasons, imdbData, -}: DetailsHeaderProps) { +}: DetailsBodyProps) { const [releaseInfo, setReleaseInfo] = useState( null, ); @@ -44,7 +44,7 @@ export function DetailsHeader({ }, [data.id]); const getQualityIndicator = () => { - if (!releaseInfo) return null; + if (!releaseInfo || data.type === "show") return null; const hasDigitalRelease = !!releaseInfo.digital_release_date; const hasTheatricalRelease = !!releaseInfo.theatrical_release_date; @@ -176,17 +176,19 @@ export function DetailsHeader({
- + {imdbData?.trailer_url && ( + + )} - setShowTrailer(true)} diff --git a/src/components/overlays/details/DetailsRatings.tsx b/src/components/overlays/details/DetailsRatings.tsx index f5d696c3..3096a81b 100644 --- a/src/components/overlays/details/DetailsRatings.tsx +++ b/src/components/overlays/details/DetailsRatings.tsx @@ -6,29 +6,11 @@ import { getRTIcon } from "@/utils/rottenTomatoesScraper"; import { DetailsRatingsProps } from "./types"; export function DetailsRatings({ - voteAverage, - voteCount, - imdbData, rtData, mediaId, mediaType, imdbId, }: DetailsRatingsProps) { - const formatVoteCount = (count?: number) => { - if (!count) return "0"; - if (count >= 1000) { - return `${Math.floor(count / 1000)}K+`; - } - return count.toString(); - }; - - const getRatingColor = (rating: number) => { - if (rating >= 8) return "bg-green-500"; - if (rating >= 6) return "bg-yellow-500"; - if (rating >= 4) return "bg-orange-500"; - return "bg-red-500"; - }; - return (
{/* External Links */} @@ -90,27 +72,6 @@ export function DetailsRatings({
)}
-
- {voteAverage !== undefined && - voteCount !== undefined && - voteCount > 0 && ( - <> - {/* Rating Progress Bar */} -
-
-
-
- {formatVoteCount(imdbData?.votes || voteCount)}{" "} - {t("details.votes")} -
- - )} -
); } diff --git a/src/components/overlays/details/old.DetailsModal.txt b/src/components/overlays/details/old.DetailsModal.txt deleted file mode 100644 index 0b9ee010..00000000 --- a/src/components/overlays/details/old.DetailsModal.txt +++ /dev/null @@ -1,1119 +0,0 @@ -import classNames from "classnames"; -import { t } from "i18next"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Helmet } from "react-helmet-async"; -import { Trans } from "react-i18next"; -import { Link } from "react-router-dom"; -import { useCopyToClipboard } from "react-use"; - -import { - getMediaBackdrop, - getMediaDetails, - getMediaLogo, -} from "@/backend/metadata/tmdb"; -import { - TMDBContentTypes, - TMDBMovieData, - TMDBShowData, -} from "@/backend/metadata/types/tmdb"; -import { Dropdown } from "@/components/form/Dropdown"; -import { Icon, Icons } from "@/components/Icon"; -import { MediaBookmarkButton } from "@/components/media/MediaBookmark"; -import { hasAired } from "@/components/player/utils/aired"; -import { useLanguageStore } from "@/stores/language"; -import { usePreferencesStore } from "@/stores/preferences"; -import { useProgressStore } from "@/stores/progress"; -import { shouldShowProgress } from "@/stores/progress/utils"; -import { scrapeIMDb } from "@/utils/imdbScraper"; -import { getTmdbLanguageCode } from "@/utils/language"; -import { getRTIcon, scrapeRottenTomatoes } from "@/utils/rottenTomatoesScraper"; - -import { useModal } from "./Modal"; -import { OverlayPortal } from "./OverlayDisplay"; -import { Button } from "../buttons/Button"; -import { IconPatch } from "../buttons/IconPatch"; -import { Flare } from "../utils/Flare"; - -function DetailsSkeleton() { - return ( -
-
- {/* Backdrop */} -
-
-
- {/* Content */} -
-
{/* Title */} -
- {/* Description */} -
-
-
-
-
- {/* Additional details */} -
-
-
-
-
-
- {/* Genres */} -
-
-
-
-
-
-
-
- ); -} - -interface DetailsContent { - title: string; - overview?: string; - backdrop?: string; - runtime?: number | null; - genres?: Array<{ id: number; name: string }>; - language?: string; - voteAverage?: number; - voteCount?: number; - releaseDate?: string; - rating?: string; - director?: string; - actors?: string[]; - type?: "movie" | "show"; - id?: number; - episodes?: number; - seasons?: number; - imdbId?: string; - episode?: { - id: number; - number: number; - }; - seasonData?: { - seasons: Array<{ - id: number; - season_number: number; - name: string; - episode_count: number; - overview: string; - air_date: string; - poster_path: string | null; - }>; - episodes: Array<{ - id: number; - name: string; - overview: string; - episode_number: number; - season_number: number; - still_path: string | null; - air_date: string; - vote_average: number; - vote_count: number; - }>; - }; - logoUrl?: string; -} - -function TrailerOverlay({ - trailerUrl, - onClose, -}: { - trailerUrl: string; - onClose: () => void; -}) { - return ( -
-
e.stopPropagation()} - > - - - {/* Close Button */} - -
-
- ); -} - -function DetailsContent({ - data, - minimal = false, -}: { - data: DetailsContent; - minimal?: boolean; -}) { - const [imdbData, setImdbData] = useState(null); - const [rtData, setRtData] = useState(null); - const [, setIsLoadingImdb] = useState(false); - const [showTrailer, setShowTrailer] = useState(false); - const [showEpisodeMenu, setShowEpisodeMenu] = useState(false); - const [customSeason, setCustomSeason] = useState(""); - const [customEpisode, setCustomEpisode] = useState(""); - const episodeMenuRef = useRef(null); - const videoRef = useRef(null); - const progress = useProgressStore((s) => s.items); - const carouselRef = useRef(null); - const activeEpisodeRef = useRef(null); - const [showVolumeBar, setShowVolumeBar] = useState(false); - const [volume, setVolume] = useState(0.5); - - const enableImageLogos = usePreferencesStore( - (state) => state.enableImageLogos, - ); - const [, copyToClipboard] = useCopyToClipboard(); - const [hasCopiedShare, setHasCopiedShare] = useState(false); - - const showProgress = useMemo(() => { - if (!data.id) return null; - const item = progress[data.id.toString()]; - if (!item) return null; - return shouldShowProgress(item); - }, [data.id, progress]); - - // Set initial season based on current episode - const [selectedSeason, setSelectedSeason] = useState(() => { - if (showProgress?.season?.number) { - return showProgress.season.number; - } - return 1; - }); - - // Update selected season when showProgress changes - useEffect(() => { - if (showProgress?.season?.number) { - setSelectedSeason(showProgress.season.number); - } - }, [showProgress]); - - useEffect(() => { - const fetchExternalData = async () => { - if (!data.imdbId) return; - - setIsLoadingImdb(true); - try { - // Get the user's selected language and format it properly - const userLanguage = useLanguageStore.getState().language; - const formattedLanguage = getTmdbLanguageCode(userLanguage); - - // Fetch IMDb data - const imdbMetadata = await scrapeIMDb( - data.imdbId, - undefined, - undefined, - formattedLanguage, - ); - setImdbData(imdbMetadata); - - // Fetch Rotten Tomatoes data - if (data.type === "movie") { - const rtMetadata = await scrapeRottenTomatoes( - data.title, - data.releaseDate - ? new Date(data.releaseDate).getFullYear() - : undefined, - ); - setRtData(rtMetadata); - } - } catch (error) { - console.error("Failed to fetch external data:", error); - } finally { - setIsLoadingImdb(false); - } - }; - - fetchExternalData(); - }, [data.imdbId, data.title, data.releaseDate, data.type]); - - const formatRuntime = (minutes?: number | null) => { - if (!minutes) return null; - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; - return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; - }; - - const getEndTime = (runtime?: number | null) => { - if (!runtime) return null; - const now = new Date(); - const endTime = new Date(now.getTime() + runtime * 60000); - return endTime.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }); - }; - - const formatVoteCount = (count?: number) => { - if (!count) return "0"; - if (count >= 1000) { - return `${Math.floor(count / 1000)}K+`; - } - return count.toString(); - }; - - const formatDate = (dateString?: string) => { - if (!dateString) return null; - return new Date(dateString).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }); - }; - - // Function to get color based on rating - const getRatingColor = (rating: number) => { - if (rating >= 8) return "bg-green-500"; - if (rating >= 6) return "bg-yellow-500"; - if (rating >= 4) return "bg-orange-500"; - return "bg-red-500"; - }; - - const handleScroll = (direction: "left" | "right") => { - if (!carouselRef.current) return; - - const cardWidth = 256; // w-64 in pixels - const cardSpacing = 16; // space-x-4 in pixels - const scrollAmount = (cardWidth + cardSpacing) * 2; - - const newScrollPosition = - carouselRef.current.scrollLeft + - (direction === "left" ? -scrollAmount : scrollAmount); - - carouselRef.current.scrollTo({ - left: newScrollPosition, - behavior: "smooth", - }); - }; - - const currentSeasonEpisodes = data.seasonData?.episodes.filter( - (ep) => ep.season_number === selectedSeason, - ); - - // Function to generate the episode URL - const getEpisodeUrl = (episode: any) => { - // Find the season ID for the current season - const season = data.seasonData?.seasons.find( - (s) => s.season_number === selectedSeason, - ); - - if (!season || !data.id) return "#"; - - // Create the URL in the format: /media/tmdb-tv-{showId}-{showName}/{seasonId}/{episodeId} - return `/media/tmdb-tv-${data.id}-${data.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}/${season.id}/${episode.id}`; - }; - - useEffect(() => { - if (carouselRef.current) { - if (activeEpisodeRef.current) { - // If there's an active episode, scroll to it - const containerLeft = carouselRef.current.getBoundingClientRect().left; - const containerWidth = carouselRef.current.clientWidth; - const elementLeft = - activeEpisodeRef.current.getBoundingClientRect().left; - const elementWidth = activeEpisodeRef.current.clientWidth; - - const scrollPosition = - elementLeft - containerLeft - containerWidth / 2 + elementWidth / 2; - - carouselRef.current.scrollTo({ - left: carouselRef.current.scrollLeft + scrollPosition, - behavior: "smooth", - }); - } else { - // If no active episode, scroll to the start - carouselRef.current.scrollTo({ - left: 0, - behavior: "smooth", - }); - } - } - }, [currentSeasonEpisodes, showProgress]); - - useEffect(() => { - if (videoRef.current) { - videoRef.current.volume = 0.4; - } - }, []); - - // Add click outside handler for episode menu - useEffect(() => { - if (!showEpisodeMenu) return; - - const handleClickOutside = (event: MouseEvent) => { - if ( - episodeMenuRef.current && - !episodeMenuRef.current.contains(event.target as Node) - ) { - setShowEpisodeMenu(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [showEpisodeMenu]); - - const handleCustomNavigation = () => { - const season = parseInt(customSeason, 10); - const episode = parseInt(customEpisode, 10); - - if (Number.isNaN(season) || Number.isNaN(episode) || !data.id) return; - - // Find the season - const seasonData = data.seasonData?.seasons.find( - (s) => s.season_number === season, - ); - if (!seasonData) return; - - // Find the episode - const episodeData = data.seasonData?.episodes.find( - (e) => e.season_number === season && e.episode_number === episode, - ); - if (!episodeData) return; - - // Navigate to the episode - window.location.assign( - `/media/tmdb-tv-${data.id}-${data.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}/${seasonData.id}/${episodeData.id}`, - ); - setShowEpisodeMenu(false); - }; - - const handleShareClick = () => { - const shareUrl = - data.type === "movie" - ? `${window.location.origin}/media/tmdb-movie-${data.id}-${data.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}` - : `${window.location.origin}/media/tmdb-tv-${data.id}-${data.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; - - copyToClipboard(shareUrl); - setHasCopiedShare(true); - setTimeout(() => setHasCopiedShare(false), 2000); - }; - - return ( -
- {/* Share notification popup */} - {hasCopiedShare && ( -
-
- - - Link copied to clipboard! - -
-
- )} - - {/* Trailer Overlay */} - {showTrailer && imdbData?.trailer_url && ( - setShowTrailer(false)} - /> - )} - - {/* Backdrop - Even taller */} -
-
-
- {/* Content */} -
- {/* Title and Genres Row */} -
- {data.logoUrl && enableImageLogos ? ( - {data.title} - ) : ( -

- {data.title} -

- )} -
-
-
- {!minimal && ( - - )} -
- {imdbData?.trailer_url && ( - - )} - - -
-
- - {/* Genres on the right side of the button row for larger screens */} - {data.genres && data.genres.length > 0 && ( -
- {data.genres.map((genre, index) => ( - - {genre.name} - - ))} -
- )} -
- - {/* Genres below for small screens */} - {data.genres && data.genres.length > 0 && ( -
- {data.genres.map((genre, index) => ( - - {genre.name} - - ))} -
- )} - - {/* Two Column Layout - Stacked on Mobile */} -
- {/* Left Column - Description */} -
- {data.overview && ( -

{data.overview}

- )} - - {/* Director and Cast */} -
- {data.director && ( -
- - {t("details.director")} - {" "} - {data.director} -
- )} - {data.actors && data.actors.length > 0 && ( -
- - {t("details.cast")} - {" "} - - {data.actors.join(", ")} - -
- )} -
-
- - {/* Right Column - Details */} -
-
- {data.runtime && ( -
-
- {t("details.runtime")}{" "} - {formatRuntime(data.runtime)} -
- {data.type === "movie" && ( -
- - -
- )} -
- )} - {data.language && ( -
- {t("details.language")}{" "} - {data.language.toUpperCase()} -
- )} - {data.releaseDate && ( -
- - {t("details.releaseDate")} - {" "} - {formatDate(data.releaseDate)} -
- )} - {data.rating && ( -
- {t("details.rating")}{" "} - {data.rating} -
- )} - {/* Rating and External Links Section */} -
- {data.voteAverage !== undefined && - data.voteCount !== undefined && - data.voteCount > 0 && ( - <> -
- - {t("details.rating")} - {" "} - - {imdbData?.imdb_rating - ? `${imdbData.imdb_rating.toFixed(1)}/10 (IMDb)` - : `${data.voteAverage.toFixed(1)}/10 (TMDB)`} - -
- {/* Rating Progress Bar */} -
-
-
-
- {formatVoteCount(imdbData?.votes || data.voteCount)}{" "} - {t("details.votes")} -
- - )} - - {/* External Links - Now outside the vote count condition */} -
- {data.id && ( - - - - )} - {data.imdbId && ( - - - - )} - {rtData && ( -
-
-
- Tomatometer - - {rtData.tomatoScore}% - -
-
-
- )} -
-
-
-
-
- - {/* Episodes Carousel for TV Shows */} - {data.type === "show" && data.seasonData && !minimal && ( -
- {/* Season Selector */} -
-
-

- {t("details.episodes")} -

-
- - - {/* Episode Selection Menu */} - {showEpisodeMenu && ( -
-
-
- - setCustomSeason(e.target.value)} - min="1" - max={data.seasonData.seasons.length} - className="w-full px-3 py-2 bg-white/5 rounded border border-white/10 text-white focus:outline-none focus:border-white/30" - placeholder={t("details.season")} - /> -
-
- - setCustomEpisode(e.target.value)} - min="1" - className="w-full px-3 py-2 bg-white/5 rounded border border-white/10 text-white focus:outline-none focus:border-white/30" - placeholder={t("details.episode")} - /> -
- -
-
- )} -
-
- ({ - id: season.season_number.toString(), - name: `${t("details.season")} ${season.season_number}`, - }))} - selectedItem={{ - id: selectedSeason.toString(), - name: `${t("details.season")} ${selectedSeason}`, - }} - setSelectedItem={(item) => setSelectedSeason(Number(item.id))} - /> -
- - {/* Episodes Carousel */} -
- {/* Left scroll button */} -
- -
- -
- {/* Add padding before the first card */} -
- - {currentSeasonEpisodes?.map((episode) => { - const isActive = - showProgress?.episode?.id === episode.id.toString(); - const episodeProgress = - progress[data.id?.toString() ?? ""]?.episodes?.[episode.id]; - const percentage = episodeProgress - ? (episodeProgress.progress.watched / - episodeProgress.progress.duration) * - 100 - : 0; - const isAired = hasAired(episode.air_date); - - return ( - - {/* Thumbnail */} -
- {episode.still_path ? ( - {episode.name} - ) : ( -
- -
- )} - - {/* Episode Number Badge */} -
- - {t("media.episodeShort")} - {episode.episode_number} - - {!isAired && ( - - {episode.air_date - ? `(${t("details.airs")} - ${new Date(episode.air_date).toLocaleDateString()})` - : `(${t("media.unreleased")})`} - - )} -
-
- - {/* Content */} -
-

- {episode.name} -

- {episode.overview && ( -

- {episode.overview} -

- )} -
- - {/* Progress Bar */} - {percentage > 0 && ( -
-
98 ? 100 : percentage}%`, - }} - /> -
- )} - - ); - })} - - {/* Add padding after the last card */} -
-
- - {/* Right scroll button */} -
- -
-
-
- )} -
-
- ); -} - -export function DetailsModal(props: { - id: string; - data?: { - id: number; - type: "movie" | "show"; - }; - minimal?: boolean; -}) { - const modal = useModal(props.id); - const [detailsData, setDetailsData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const fetchDetails = async () => { - if (!props.data?.id || !props.data?.type) return; - - setIsLoading(true); - try { - const type = - props.data.type === "movie" - ? TMDBContentTypes.MOVIE - : TMDBContentTypes.TV; - const details = await getMediaDetails(props.data.id.toString(), type); - const backdropUrl = getMediaBackdrop(details.backdrop_path); - const logoUrl = await getMediaLogo(props.data.id.toString(), type); - if (type === TMDBContentTypes.MOVIE) { - const movieDetails = details as TMDBMovieData; - setDetailsData({ - title: movieDetails.title, - overview: movieDetails.overview, - backdrop: backdropUrl, - runtime: movieDetails.runtime, - genres: movieDetails.genres, - language: movieDetails.original_language, - voteAverage: movieDetails.vote_average, - voteCount: movieDetails.vote_count, - releaseDate: movieDetails.release_date, - rating: movieDetails.release_dates?.results?.find( - (r) => r.iso_3166_1 === "US", - )?.release_dates?.[0]?.certification, - director: movieDetails.credits?.crew?.find( - (person) => person.job === "Director", - )?.name, - actors: movieDetails.credits?.cast - ?.slice(0, 5) - .map((actor) => actor.name), - type: "movie", - id: movieDetails.id, - imdbId: movieDetails.external_ids?.imdb_id, - logoUrl, - }); - } else { - const showDetails = details as TMDBShowData & { - episodes: Array<{ - id: number; - name: string; - episode_number: number; - overview: string; - still_path: string | null; - air_date: string; - season_number: number; - }>; - }; - setDetailsData({ - title: showDetails.name, - overview: showDetails.overview, - backdrop: backdropUrl, - episodes: showDetails.number_of_episodes, - seasons: showDetails.number_of_seasons, - genres: showDetails.genres, - language: showDetails.original_language, - voteAverage: showDetails.vote_average, - voteCount: showDetails.vote_count, - releaseDate: showDetails.first_air_date, - rating: showDetails.content_ratings?.results?.find( - (r) => r.iso_3166_1 === "US", - )?.rating, - director: showDetails.credits?.crew?.find( - (person) => person.job === "Director", - )?.name, - actors: showDetails.credits?.cast - ?.slice(0, 5) - .map((actor) => actor.name), - type: "show", - id: showDetails.id, - imdbId: showDetails.external_ids?.imdb_id, - seasonData: { - seasons: showDetails.seasons, - episodes: showDetails.episodes, - }, - logoUrl, - }); - } - } catch (err) { - console.error("Failed to fetch media details:", err); - } finally { - setIsLoading(false); - } - }; - - if (modal.isShown && props.data?.id) { - fetchDetails(); - } - }, [modal.isShown, props.data]); - - useEffect(() => { - if (modal.isShown && !props.data?.id && !isLoading) { - modal.hide(); - } - }, [modal, props.data, isLoading]); - - return ( - - - - -
- -
- - -
- -
-
- {isLoading || !detailsData ? ( - - ) : ( - - )} -
-
-
-
-
-
- ); -} diff --git a/src/components/overlays/details/types.ts b/src/components/overlays/details/types.ts index ae474d17..33aa2f50 100644 --- a/src/components/overlays/details/types.ts +++ b/src/components/overlays/details/types.ts @@ -99,7 +99,7 @@ export interface EpisodeCarouselProps { mediaTitle?: string; } -export interface DetailsHeaderProps { +export interface DetailsBodyProps { data: DetailsContent; onPlayClick: () => void; onTrailerClick: () => void; @@ -112,6 +112,7 @@ export interface DetailsHeaderProps { imdbData?: { rating: number; votes: number; + trailer_url?: string; }; }