From 88e03fd08db2ce1ac9a55a3b37b4b8469a703057 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:05:02 -0600 Subject: [PATCH] OLD 3.2.1 remove discover and fix some stuff fix some color errors init old breaking shit but idc 1 --- src/components/buttons/IconPatch.tsx | 8 +- src/components/form/SearchBar.tsx | 129 ++--- src/components/layout/BrandPill.tsx | 20 +- src/components/layout/Footer.tsx | 108 ---- src/components/layout/IconPill.tsx | 2 +- src/components/layout/Navigation.tsx | 130 ++--- src/components/layout/ProgressRing.tsx | 2 +- src/components/layout/WideContainer.tsx | 4 +- src/components/media/MediaCard.tsx | 487 +++------------- src/components/media/MediaGrid.tsx | 5 +- src/components/media/WatchedMediaCard.tsx | 2 - src/components/text/GlitchText.tsx | 71 +++ src/components/text/HeroTitle.tsx | 4 +- src/pages/HomePage.tsx | 214 ++----- src/pages/Settings.tsx | 29 +- src/pages/discover/Discover.tsx | 49 -- src/pages/discover/common.ts | 59 -- .../components/CarouselNavButtons.tsx | 83 --- .../discover/components/CategoryButtons.tsx | 68 --- .../discover/components/LazyMediaCarousel.tsx | 95 ---- .../discover/components/LazyTabContent.tsx | 29 - .../discover/components/MediaCarousel.tsx | 167 ------ .../discover/components/RandomMovieButton.tsx | 63 --- .../discover/components/ScrollToTopButton.tsx | 55 -- src/pages/discover/discover.css | 56 -- src/pages/discover/discoverContent.tsx | 526 ------------------ .../hooks/useIntersectionObserver.tsx | 43 -- src/pages/discover/hooks/useTMDBData.tsx | 147 ----- src/pages/layouts/HomeLayout.tsx | 5 +- src/pages/layouts/PageLayout.tsx | 5 +- src/pages/layouts/SubPageLayout.tsx | 7 +- src/pages/parts/auth/LoginFormPart.tsx | 2 +- src/pages/parts/home/BookmarksPart.tsx | 3 - src/pages/parts/home/HeroPart.tsx | 9 +- src/pages/parts/home/WatchingPart.tsx | 28 +- src/pages/parts/player/PlayerPart.tsx | 52 +- src/pages/parts/player/ScrapingPart.tsx | 167 +++--- src/pages/parts/search/SearchListPart.tsx | 14 +- src/pages/parts/settings/AppearancePart.tsx | 40 -- src/pages/parts/settings/SidebarPart.tsx | 24 +- src/setup/App.tsx | 3 - src/utils/setup/App.tsx | 3 - tailwind.config.ts | 33 +- themes/all.ts | 16 +- themes/default.ts | 362 +++++------- 45 files changed, 597 insertions(+), 2831 deletions(-) delete mode 100644 src/components/layout/Footer.tsx create mode 100644 src/components/text/GlitchText.tsx delete mode 100644 src/pages/discover/Discover.tsx delete mode 100644 src/pages/discover/common.ts delete mode 100644 src/pages/discover/components/CarouselNavButtons.tsx delete mode 100644 src/pages/discover/components/CategoryButtons.tsx delete mode 100644 src/pages/discover/components/LazyMediaCarousel.tsx delete mode 100644 src/pages/discover/components/LazyTabContent.tsx delete mode 100644 src/pages/discover/components/MediaCarousel.tsx delete mode 100644 src/pages/discover/components/RandomMovieButton.tsx delete mode 100644 src/pages/discover/components/ScrollToTopButton.tsx delete mode 100644 src/pages/discover/discover.css delete mode 100644 src/pages/discover/discoverContent.tsx delete mode 100644 src/pages/discover/hooks/useIntersectionObserver.tsx delete mode 100644 src/pages/discover/hooks/useTMDBData.tsx diff --git a/src/components/buttons/IconPatch.tsx b/src/components/buttons/IconPatch.tsx index 945c91bb..d51f20b1 100644 --- a/src/components/buttons/IconPatch.tsx +++ b/src/components/buttons/IconPatch.tsx @@ -7,25 +7,23 @@ export interface IconPatchProps { className?: string; icon: Icons; transparent?: boolean; - downsized?: boolean; } export function IconPatch(props: IconPatchProps) { const clickableClasses = props.clickable - ? "cursor-pointer hover:scale-110 hover:bg-pill-backgroundHover hover:text-white active:scale-125" + ? "cursor-pointer hover:scale-110 hover:bg-denim-600 hover:text-white active:scale-125" : ""; const transparentClasses = props.transparent ? "bg-opacity-0 hover:bg-opacity-50" : ""; const activeClasses = props.active - ? "bg-pill-backgroundHover text-white" + ? "border-bink-600 bg-bink-100 text-bink-600" : ""; - const sizeClasses = props.downsized ? "h-10 w-10" : "h-12 w-12"; return (
diff --git a/src/components/form/SearchBar.tsx b/src/components/form/SearchBar.tsx index 88d52538..c4a0fb6e 100644 --- a/src/components/form/SearchBar.tsx +++ b/src/components/form/SearchBar.tsx @@ -1,7 +1,5 @@ import c from "classnames"; -import { forwardRef, useRef, useState } from "react"; - -import { Flare } from "@/components/utils/Flare"; +import { forwardRef, useState } from "react"; import { Icon, Icons } from "../Icon"; import { TextInputControl } from "../text-inputs/TextInputControl"; @@ -16,103 +14,50 @@ export interface SearchBarProps { export const SearchBarInput = forwardRef( (props, ref) => { const [focused, setFocused] = useState(false); - const containerRef = useRef(null); - const [showTooltip, setShowTooltip] = useState(false); function setSearch(value: string) { props.onChange(value, true); } return ( -
- - - -
{ - e.preventDefault(); - setShowTooltip(!showTooltip); - if (ref && typeof ref !== "function" && ref.current) { - ref.current.focus(); - } - }} - > - -
+
+
+ +
- { - setFocused(false); - props.onUnFocus(); - }} - onFocus={() => setFocused(true)} - onChange={(val) => setSearch(val)} - value={props.value} - className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2" - placeholder={props.placeholder} - /> + { + setFocused(false); + props.onUnFocus(); + }} + onFocus={() => setFocused(true)} + onChange={(val) => setSearch(val)} + value={props.value} + className="w-full flex-1 bg-transparent py-4 pl-12 text-white placeholder-denim-700 focus:outline-none sm:py-4 sm:pr-2" + placeholder={props.placeholder} + /> - {showTooltip && ( -
-

- Advanced Search: -

-
-
-

Year search:

-

- Inception year:2010 -

-
-
-

TMDB ID search:

-

- tmdb:123456 - For movies -

-

- tmdb:123456:tv - For TV shows -

-
-
-
- )} - - {props.value.length > 0 && ( -
{ - props.onUnFocus(""); - if (ref && typeof ref !== "function") { - ref.current?.focus(); - } - }} - className="cursor-pointer hover:text-white absolute bottom-0 right-2 top-0 flex justify-center my-auto h-10 w-10 items-center hover:bg-search-hoverBackground active:scale-110 text-search-icon rounded-full transition-[transform,background-color] duration-200" - > - -
- )} - - + {props.value.length > 0 && ( +
{ + props.onUnFocus(""); + if (ref && typeof ref !== "function") { + ref.current?.focus(); + } + }} + className="cursor-pointer hover:text-white absolute bottom-0 right-2 top-0 flex justify-center my-auto h-10 w-10 items-center hover:bg-denim-600 active:scale-110 text-white rounded-full transition-[transform,background-color] duration-200" + > + +
+ )}
); }, diff --git a/src/components/layout/BrandPill.tsx b/src/components/layout/BrandPill.tsx index ac380b38..9ccbaa4d 100644 --- a/src/components/layout/BrandPill.tsx +++ b/src/components/layout/BrandPill.tsx @@ -1,32 +1,26 @@ -import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { Icon, Icons } from "@/components/Icon"; -import { useIsMobile } from "@/hooks/useIsMobile"; export function BrandPill(props: { clickable?: boolean; - header?: boolean; - backgroundClass?: string; + hideTextOnMobile?: boolean; }) { const { t } = useTranslation(); - const isMobile = useIsMobile(); return (
- + {t("global.name")} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx deleted file mode 100644 index 7a8ca493..00000000 --- a/src/components/layout/Footer.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import type { RequireExactlyOne } from "type-fest"; - -import { Icon, Icons } from "@/components/Icon"; -import { BrandPill } from "@/components/layout/BrandPill"; -import { WideContainer } from "@/components/layout/WideContainer"; -import { conf } from "@/setup/config"; - -// to and href are mutually exclusive -type FooterLinkProps = RequireExactlyOne< - { - children: React.ReactNode; - icon: Icons; - to: string; - href: string; - }, - "to" | "href" ->; - -function FooterLink(props: FooterLinkProps) { - const navigate = useNavigate(); - - const navigateTo = useCallback(() => { - if (!props.to) return; - - navigate(props.to); - }, [navigate, props.to]); - - return ( - - - {props.children} - - ); -} - -function Dmca() { - const { t } = useTranslation(); - - if (window.location.hash === "#/dmca") return null; - - return ( - - {t("footer.links.dmca")} - - ); -} - -export function Footer() { - const { t } = useTranslation(); - - return ( -
- -
-
- -
-

{t("footer.tagline")}

-
-
-

- {t("footer.legal.disclaimer")} -

-

{t("footer.legal.disclaimerText")}

-
-
- - {t("footer.links.discord")} - - - Support us - -
- -
-
-
- -
-
-
- ); -} - -export function FooterView(props: { - children: React.ReactNode; - className?: string; -}) { - return ( -
-
{props.children}
-
-
- ); -} diff --git a/src/components/layout/IconPill.tsx b/src/components/layout/IconPill.tsx index 6530e773..2ae853c3 100644 --- a/src/components/layout/IconPill.tsx +++ b/src/components/layout/IconPill.tsx @@ -2,7 +2,7 @@ import { Icon, Icons } from "@/components/Icon"; export function IconPill(props: { icon: Icons; children?: React.ReactNode }) { return ( -
+
{ window.scrollTo(0, 0); @@ -30,93 +20,53 @@ export function Navigation(props: NavigationProps) { }; return ( - <> - {/* lightbar */} - {!props.noLightbar ? ( +
+
-
- +
+
+ +
+
+ window.scrollTo(0, 0)}> + +
- ) : null} - {/* backgrounds - these are seperate because of z-index issues */} -
- ); } diff --git a/src/components/layout/ProgressRing.tsx b/src/components/layout/ProgressRing.tsx index d0e680a9..6e3f93ac 100644 --- a/src/components/layout/ProgressRing.tsx +++ b/src/components/layout/ProgressRing.tsx @@ -14,7 +14,7 @@ export function ProgressRing(props: Props) { viewBox="0 0 100 100" > {props.children} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index ed446fd2..9ea82114 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,24 +1,16 @@ // I'm sorry this is so confusing 😭 import classNames from "classnames"; -import { useCallback, useRef, useState } from "react"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { useCopyToClipboard } from "react-use"; import { mediaItemToId } from "@/backend/metadata/tmdb"; import { DotList } from "@/components/text/DotList"; -import { Flare } from "@/components/utils/Flare"; -import { useSearchQuery } from "@/hooks/useSearchQuery"; -import { usePreferencesStore } from "@/stores/preferences"; import { MediaItem } from "@/utils/mediaTypes"; -import { MediaBookmarkButton } from "./MediaBookmark"; -import { Button } from "../buttons/Button"; import { IconPatch } from "../buttons/IconPatch"; -import { Icon, Icons } from "../Icon"; -import { DetailsModal } from "../overlays/DetailsModal"; -import { useModal } from "../overlays/Modal"; +import { Icons } from "../Icon"; export interface MediaCardProps { media: MediaItem; @@ -32,7 +24,6 @@ export interface MediaCardProps { percentage?: number; closable?: boolean; onClose?: () => void; - onShowDetails?: (media: MediaItem) => void; } function checkReleased(media: MediaItem): boolean { @@ -60,43 +51,14 @@ function MediaCardContent({ percentage, closable, onClose, - overlayVisible, - setOverlayVisible, - handleMouseEnter, - handleMouseLeave, - link, - isHoveringCard, - onShowDetails, -}: MediaCardProps & { - overlayVisible: boolean; - setOverlayVisible: React.Dispatch>; - handleMouseEnter: () => void; - handleMouseLeave: () => void; - link: string; - isHoveringCard: boolean; -}) { +}: MediaCardProps) { const { t } = useTranslation(); const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`; const isReleased = useCallback(() => checkReleased(media), [media]); - const canLink = linkable && !closable && isReleased(); - const dotListContent = [t(`media.types.${media.type}`)]; - const altDotListContent = [t(`ID: ${media.id}`)]; - - const [searchQuery] = useSearchQuery(); - - const [, copyToClipboard] = useCopyToClipboard(); - const [hasCopied, setHasCopied] = useState(false); - - const [hasCopiedID, setHasCopiedID] = useState(false); - - if (closable) { - setOverlayVisible(false); - } - if (isReleased() && media.year) { dotListContent.push(media.year.toFixed()); } @@ -105,311 +67,100 @@ function MediaCardContent({ dotListContent.push(t("media.unreleased")); } - const handleCopyClick = ( - e: React.MouseEvent, - ) => { - e.preventDefault(); - copyToClipboard(link); - setHasCopied(true); - setTimeout(() => setHasCopied(false), 2000); - }; - - const handleCopyIDClick = ( - e: React.MouseEvent, - ) => { - e.preventDefault(); - copyToClipboard(media.id); - setHasCopiedID(true); - setTimeout(() => setHasCopiedID(false), 2000); - }; - return ( -
- +
e.key === "Enter" && e.currentTarget.click()} > - -
- {!overlayVisible ? ( -
- {series ? ( -
-

- {t("media.episodeDisplay", { - season: series.season || 1, - episode: series.episode, - })} -

-
- ) : null} - - {percentage !== undefined ? ( - <> -
-
-
-
-
-
-
- - ) : null} + {series ? ( +
+

+ {t("media.episodeDisplay", { + season: series.season || 1, + episode: series.episode, + })} +

) : null} - {!overlayVisible ? ( -
- {!closable ? ( -
-
e.preventDefault()} - > - -
- {searchQuery.length > 0 ? ( -
e.preventDefault()} - > - -
- ) : null} -
- ) : null} + {percentage !== undefined ? ( + <>
- closable && onClose?.()} - icon={Icons.X} - /> + /> +
+
+
+
-
+ ) : null} -
- {overlayVisible ? ( -
-
- - - {canLink ? ( - - ) : null} - - -
+
+ closable && onClose?.()} + icon={Icons.X} + />
- ) : null} +

{media.title}

-
- {!overlayVisible ? ( - - ) : ( - - )} -
- - {!overlayVisible && !closable ? ( -
- -
- ) : null} - - + + +
); } export function MediaCard(props: MediaCardProps) { - const { media, onShowDetails } = props; - const [overlayVisible, setOverlayVisible] = useState(false); - const [timeoutId, setTimeoutId] = useState(null); - const hoverTimer = useRef(); - const [isHoveringCard, setIsHoveringCard] = useState(false); - const [detailsData, setDetailsData] = useState<{ - id: number; - type: "movie" | "show"; - } | null>(null); - const detailsModal = useModal("details"); - const enableDetailsModal = usePreferencesStore( - (state) => state.enableDetailsModal, - ); - - const handleMouseEnter = () => { - setIsHoveringCard(true); - - if (timeoutId) { - clearTimeout(timeoutId); - setTimeoutId(null); - } - - if (hoverTimer.current) { - clearTimeout(hoverTimer.current); - } - }; - - const handleMouseLeave = () => { - setIsHoveringCard(false); - if (hoverTimer.current) { - clearTimeout(hoverTimer.current); - } - - const id = setTimeout(() => { - setOverlayVisible(false); - }, 2000); // 2 seconds - setTimeoutId(id); - }; - - const handleContextMenu = (e: React.MouseEvent) => { - e.preventDefault(); - setOverlayVisible(true); - }; - + const content = ; const isReleased = useCallback( () => checkReleased(props.media), [props.media], ); - const canLink = props.linkable && !props.closable && isReleased(); let link = canLink @@ -425,109 +176,13 @@ export function MediaCard(props: MediaCardProps) { } } - const handleShowDetails = useCallback(async () => { - if (onShowDetails) { - onShowDetails(media); - return; - } - - setDetailsData({ - id: Number(media.id), - type: media.type === "movie" ? "movie" : "show", - }); - detailsModal.show(); - }, [media, detailsModal, onShowDetails]); - - const handleCardClick = (e: React.MouseEvent) => { - if (enableDetailsModal && canLink) { - e.preventDefault(); - handleShowDetails(); - } else if (overlayVisible || e.defaultPrevented) { - e.preventDefault(); - } - }; - - const content = ( - <> - - {detailsData && } - - ); - - if (!canLink) - return ( - { - if (overlayVisible || e.defaultPrevented) { - e.preventDefault(); - } - }} - > - {content}{" "} - - ); + if (!canLink) return {content}; return ( -
- {!overlayVisible ? ( - - - - ) : ( -
- -
- )} -
+ + {content} + ); } diff --git a/src/components/media/MediaGrid.tsx b/src/components/media/MediaGrid.tsx index 26873a10..2f3273bd 100644 --- a/src/components/media/MediaGrid.tsx +++ b/src/components/media/MediaGrid.tsx @@ -7,10 +7,7 @@ interface MediaGridProps { export const MediaGrid = forwardRef( (props, ref) => { return ( -
+
{props.children}
); diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index 4d799809..b7640efd 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -23,7 +23,6 @@ export interface WatchedMediaCardProps { media: MediaItem; closable?: boolean; onClose?: () => void; - onShowDetails?: (media: MediaItem) => void; } export function WatchedMediaCard(props: WatchedMediaCardProps) { @@ -47,7 +46,6 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) { percentage={percentage} onClose={props.onClose} closable={props.closable} - onShowDetails={props.onShowDetails} /> ); } diff --git a/src/components/text/GlitchText.tsx b/src/components/text/GlitchText.tsx new file mode 100644 index 00000000..0c457fe4 --- /dev/null +++ b/src/components/text/GlitchText.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { useInterval } from "react-use"; + +interface GlitchTextProps { + text: string; + glitchedText: string; + className?: string; + groupHover?: boolean; +} + +export function GlitchText({ + text, + glitchedText, + className = "", + groupHover = false, +}: GlitchTextProps) { + const [isGlitching, setIsGlitching] = useState(false); + const [displayText, setDisplayText] = useState(text); + const [glitchCount, setGlitchCount] = useState(0); + const maxGlitches = 10; // Number of glitch iterations before showing final text + + useEffect(() => { + if (groupHover) { + const parent = document.querySelector("[data-info-card]"); + + const handleMouseEnter = () => { + setIsGlitching(true); + setGlitchCount(0); + }; + + const handleMouseLeave = () => { + setDisplayText(text); + setIsGlitching(false); + setGlitchCount(0); + }; + + parent?.addEventListener("mouseenter", handleMouseEnter); + parent?.addEventListener("mouseleave", handleMouseLeave); + + return () => { + parent?.removeEventListener("mouseenter", handleMouseEnter); + parent?.removeEventListener("mouseleave", handleMouseLeave); + }; + } + }, [groupHover, text]); + + useInterval( + () => { + if (glitchCount >= maxGlitches) { + setDisplayText(glitchedText); + setIsGlitching(false); + setGlitchCount(0); + return; + } + + const randomChars = glitchedText + .split("") + .map(() => String.fromCharCode(33 + Math.floor(Math.random() * 94))) + .join(""); + setDisplayText(randomChars); + setGlitchCount((count) => count + 1); + }, + isGlitching ? 50 : null, + ); + + return ( + + {displayText} + + ); +} diff --git a/src/components/text/HeroTitle.tsx b/src/components/text/HeroTitle.tsx index a84c47e2..522a0071 100644 --- a/src/components/text/HeroTitle.tsx +++ b/src/components/text/HeroTitle.tsx @@ -6,9 +6,7 @@ export interface HeroTitleProps { export function HeroTitle(props: HeroTitleProps) { return (

{props.children}

diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 2df30cf5..f32bd973 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,25 +1,17 @@ +import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useTranslation } from "react-i18next"; -import { To, useNavigate } from "react-router-dom"; import { WideContainer } from "@/components/layout/WideContainer"; -import { DetailsModal } from "@/components/overlays/DetailsModal"; -import { useModal } from "@/components/overlays/Modal"; import { useDebounce } from "@/hooks/useDebounce"; -import { useRandomTranslation } from "@/hooks/useRandomTranslation"; import { useSearchQuery } from "@/hooks/useSearchQuery"; -import DiscoverContent from "@/pages/discover/discoverContent"; import { HomeLayout } from "@/pages/layouts/HomeLayout"; import { BookmarksPart } from "@/pages/parts/home/BookmarksPart"; import { HeroPart } from "@/pages/parts/home/HeroPart"; import { WatchingPart } from "@/pages/parts/home/WatchingPart"; import { SearchListPart } from "@/pages/parts/search/SearchListPart"; import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart"; -import { usePreferencesStore } from "@/stores/preferences"; -import { MediaItem } from "@/utils/mediaTypes"; - -import { Button } from "./About"; function useSearch(search: string) { const [searching, setSearching] = useState(false); @@ -44,52 +36,17 @@ function useSearch(search: string) { export function HomePage() { const { t } = useTranslation(); - const { t: randomT } = useRandomTranslation(); - const emptyText = randomT(`home.search.empty`); - const navigate = useNavigate(); const [showBg, setShowBg] = useState(false); const searchParams = useSearchQuery(); const [search] = searchParams; const s = useSearch(search); const [showBookmarks, setShowBookmarks] = useState(false); const [showWatching, setShowWatching] = useState(false); - const [detailsData, setDetailsData] = useState(); - // const [isLoadingDetails, setIsLoadingDetails] = useState(false); - const detailsModal = useModal("details"); - - const handleClick = (path: To) => { - window.scrollTo(0, 0); - navigate(path); - }; - - const enableDiscover = usePreferencesStore((state) => state.enableDiscover); - - const handleShowDetails = async (media: MediaItem) => { - setDetailsData({ - id: Number(media.id), - type: media.type === "movie" ? "movie" : "show", - }); - detailsModal.show(); - }; - - // const { loggedIn } = useAuth(); // Adjust padding for popup show button based on logged in state + const [contentRef] = useAutoAnimate(); return ( - {/* modal.show()} - className={` text-white tabbable rounded-full z-50 fixed top-5 ${ - loggedIn - ? "right-[7.5rem] lg:right-[12.5rem] lg:text-2xl" - : "right-[7.5rem] text-xl lg:text-lg" - }`} - style={{ animation: "pulse 1s infinite" }} - > - - READ - - */} -
+
- - - - -
-
-

- {t("discover.page.title")} -

-

- {t("discover.page.subtitle")} -

-
- - - - ); -} diff --git a/src/pages/discover/common.ts b/src/pages/discover/common.ts deleted file mode 100644 index 2064be1b..00000000 --- a/src/pages/discover/common.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* Define shit here */ - -// Define the Media type -export interface Media { - id: number; - poster_path: string; - title?: string; - name?: string; - release_date?: string; - first_air_date?: string; -} - -// Update the Movie and TVShow interfaces to extend the Media interface -export interface Movie extends Media { - title: string; -} - -export interface TVShow extends Media { - name: string; -} - -// Define the Genre type -export interface Genre { - id: number; - name: string; -} - -// Define the Category type -export interface Category { - name: string; - endpoint: string; -} - -// Define the categories -export const categories: Category[] = [ - { - name: "Now Playing", - endpoint: "/movie/now_playing?language=en-US", - }, - { - name: "Top Rated", - endpoint: "/movie/top_rated?language=en-US", - }, - { - name: "Most Popular", - endpoint: "/movie/popular?language=en-US", - }, -]; - -export const tvCategories: Category[] = [ - { - name: "Top Rated", - endpoint: "/tv/top_rated?language=en-US", - }, - { - name: "Most Popular", - endpoint: "/tv/popular?language=en-US", - }, -]; diff --git a/src/pages/discover/components/CarouselNavButtons.tsx b/src/pages/discover/components/CarouselNavButtons.tsx deleted file mode 100644 index bd9d3261..00000000 --- a/src/pages/discover/components/CarouselNavButtons.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Icon, Icons } from "@/components/Icon"; -import { Flare } from "@/components/utils/Flare"; - -interface CarouselNavButtonsProps { - categorySlug: string; - carouselRefs: React.MutableRefObject<{ - [key: string]: HTMLDivElement | null; - }>; -} - -interface NavButtonProps { - direction: "left" | "right"; - onClick: () => void; -} - -function NavButton({ direction, onClick }: NavButtonProps) { - return ( - - ); -} - -export function CarouselNavButtons({ - categorySlug, - carouselRefs, -}: CarouselNavButtonsProps) { - const handleScroll = (direction: "left" | "right") => { - const carousel = carouselRefs.current[categorySlug]; - if (!carousel) return; - - const movieElements = carousel.getElementsByTagName("a"); - if (movieElements.length === 0) return; - - // Wait for next frame to ensure measurements are available - requestAnimationFrame(() => { - const movieWidth = movieElements[0].getBoundingClientRect().width; - - const carouselWidth = carousel.getBoundingClientRect().width; - - if (movieWidth === 0 || carouselWidth === 0) { - return; - } - - const visibleMovies = Math.floor(carouselWidth / movieWidth); - const scrollAmount = movieWidth * (visibleMovies > 5 ? 4 : 2); - - const newScrollPosition = - carousel.scrollLeft + - (direction === "left" ? -scrollAmount : scrollAmount); - - carousel.scrollTo({ - left: newScrollPosition, - behavior: "smooth", - }); - }); - }; - - return ( - <> - handleScroll("left")} /> - handleScroll("right")} /> - - ); -} diff --git a/src/pages/discover/components/CategoryButtons.tsx b/src/pages/discover/components/CategoryButtons.tsx deleted file mode 100644 index 6bb6bfc8..00000000 --- a/src/pages/discover/components/CategoryButtons.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Icon, Icons } from "@/components/Icon"; - -interface CategoryButtonsProps { - categories: any[]; - onCategoryClick: (id: string, name: string) => void; - categoryType: string; - isMobile: boolean; - showAlwaysScroll: boolean; -} - -export function CategoryButtons({ - categories, - onCategoryClick, - categoryType, - isMobile, - showAlwaysScroll, -}: CategoryButtonsProps) { - const renderScrollButton = (direction: "left" | "right") => ( -
- -
- ); - - return ( -
- {(showAlwaysScroll || isMobile) && renderScrollButton("left")} - - - - {(showAlwaysScroll || isMobile) && renderScrollButton("right")} -
- ); -} diff --git a/src/pages/discover/components/LazyMediaCarousel.tsx b/src/pages/discover/components/LazyMediaCarousel.tsx deleted file mode 100644 index 6525fcbb..00000000 --- a/src/pages/discover/components/LazyMediaCarousel.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useEffect, useState } from "react"; - -import { Category, Genre, Media } from "@/pages/discover/common"; -import { useIntersectionObserver } from "@/pages/discover/hooks/useIntersectionObserver"; -import { useLazyTMDBData } from "@/pages/discover/hooks/useTMDBData"; -import { MediaItem } from "@/utils/mediaTypes"; - -import { MediaCarousel } from "./MediaCarousel"; - -interface LazyMediaCarouselProps { - category?: Category | null; - genre?: Genre | null; - mediaType: "movie" | "tv"; - isMobile: boolean; - carouselRefs: React.MutableRefObject<{ - [key: string]: HTMLDivElement | null; - }>; - preloadedMedia?: Media[]; - title?: string; - onShowDetails?: (media: MediaItem) => void; -} - -export function LazyMediaCarousel({ - category, - genre, - mediaType, - isMobile, - carouselRefs, - preloadedMedia, - title, - onShowDetails, -}: LazyMediaCarouselProps) { - const [medias, setMedias] = useState([]); - - // Use intersection observer to detect when this component is visible - const { targetRef, isIntersecting } = useIntersectionObserver( - { rootMargin: "200px" }, // Load when within 200px of viewport - ); - - // Use the lazy loading hook only if we don't have preloaded media - const { media, isLoading } = useLazyTMDBData( - !preloadedMedia ? genre || null : null, - !preloadedMedia ? category || null : null, - mediaType, - isIntersecting, - ); - - // Update medias when data is loaded or preloaded - useEffect(() => { - if (preloadedMedia) { - setMedias(preloadedMedia); - } else if (media.length > 0) { - setMedias(media); - } - }, [media, preloadedMedia]); - - const categoryName = title || category?.name || genre?.name || ""; - const categorySlug = `${categoryName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${mediaType}`; - - // Test intersection observer - // useEffect(() => { - // // eslint-disable-next-line no-console - // console.log( - // `Carousel ${categoryName}: ${isIntersecting ? "loaded ✅" : "unloaded ❌"}`, - // ); - // }, [isIntersecting, categoryName]); - - return ( -
}> - {isIntersecting ? ( - - ) : ( -
-
-

- {categoryName} {mediaType === "tv" ? "Shows" : "Movies"} -

-
-
- {isLoading ? "Loading..." : ""} -
-
-
-
- )} -
- ); -} diff --git a/src/pages/discover/components/LazyTabContent.tsx b/src/pages/discover/components/LazyTabContent.tsx deleted file mode 100644 index 79e59fd4..00000000 --- a/src/pages/discover/components/LazyTabContent.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ReactNode, useEffect, useState } from "react"; - -interface LazyTabContentProps { - isActive: boolean; - children: ReactNode; - preloadWhenInactive?: boolean; -} - -export function LazyTabContent({ - isActive, - children, - preloadWhenInactive = false, -}: LazyTabContentProps) { - const [hasLoaded, setHasLoaded] = useState(false); - - useEffect(() => { - // Load content when tab becomes active or if preload is enabled - if (isActive || preloadWhenInactive) { - setHasLoaded(true); - } - }, [isActive, preloadWhenInactive]); - - // Only render children if the tab has been loaded - return ( -
- {hasLoaded ? children : null} -
- ); -} diff --git a/src/pages/discover/components/MediaCarousel.tsx b/src/pages/discover/components/MediaCarousel.tsx deleted file mode 100644 index 71eeebad..00000000 --- a/src/pages/discover/components/MediaCarousel.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import { MediaCard } from "@/components/media/MediaCard"; -import { Media } from "@/pages/discover/common"; -import { MediaItem } from "@/utils/mediaTypes"; - -import { CarouselNavButtons } from "./CarouselNavButtons"; - -interface MediaCarouselProps { - medias: Media[]; - category: string; - isTVShow: boolean; - isMobile: boolean; - carouselRefs: React.MutableRefObject<{ - [key: string]: HTMLDivElement | null; - }>; - onShowDetails?: (media: MediaItem) => void; -} - -function MediaCardSkeleton() { - return ( -
-
-
-
-
-
- ); -} - -export function MediaCarousel({ - medias, - category, - isTVShow, - isMobile, - carouselRefs, - onShowDetails, -}: MediaCarouselProps) { - const { t } = useTranslation(); - const categorySlug = `${category.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${isTVShow ? "tv" : "movie"}`; - const browser = !!window.chrome; - let isScrolling = false; - - const handleWheel = (e: React.WheelEvent) => { - if (isScrolling) return; - isScrolling = true; - - if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) { - e.stopPropagation(); - e.preventDefault(); - } - - if (browser) { - setTimeout(() => { - isScrolling = false; - }, 345); - } else { - isScrolling = false; - } - }; - - function getDisplayCategory( - categoryName: string, - isTVShowCondition: boolean, - ): string { - const providerMatch = categoryName.match( - /^Popular (Movies|Shows) on (.+)$/, - ); - if (providerMatch) { - const type = providerMatch[1].toLowerCase(); - const provider = providerMatch[2]; - return t("discover.carousel.title.popularOn", { - type: - type === "movies" ? t("media.types.movie") : t("media.types.show"), - provider, - }); - } - - if (categoryName === "Now Playing") { - return t("discover.carousel.title.inCinemas"); - } - - if (categoryName === "Editor Picks") { - return t("discover.carousel.title.editorPicks"); - } - - return isTVShowCondition - ? t("discover.carousel.title.tvshows", { category: categoryName }) - : t("discover.carousel.title.movies", { category: categoryName }); - } - - const displayCategory = getDisplayCategory(category, isTVShow); - - const filteredMedias = medias - .filter( - (media, index, self) => - index === - self.findIndex((m) => m.id === media.id && m.title === media.title), - ) - .slice(0, 20); - - const SKELETON_COUNT = 10; - - return ( - <> -

- {displayCategory} -

-
-
{ - carouselRefs.current[categorySlug] = el; - }} - onWheel={handleWheel} - > -
- - {filteredMedias.length > 0 - ? filteredMedias.map((media) => ( -
) => - e.preventDefault() - } - key={media.id} - className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto" - > - -
- )) - : Array.from({ length: SKELETON_COUNT }).map(() => ( - - ))} - -
-
- - {!isMobile && ( - - )} -
- - ); -} diff --git a/src/pages/discover/components/RandomMovieButton.tsx b/src/pages/discover/components/RandomMovieButton.tsx deleted file mode 100644 index a11b8919..00000000 --- a/src/pages/discover/components/RandomMovieButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { Icon, Icons } from "@/components/Icon"; - -interface RandomMovieButtonProps { - countdown: number | null; - onClick: () => void; - randomMovieTitle: string | null; -} - -export function RandomMovieButton({ - countdown, - onClick, - randomMovieTitle, -}: RandomMovieButtonProps) { - const { t } = useTranslation(); - - return ( -
-
- -
- - {/* Random Movie Countdown */} - {randomMovieTitle && countdown !== null && ( -
-

- {t("discover.randomMovie.nowPlaying")}{" "} - {randomMovieTitle}{" "} - {t("discover.randomMovie.in")}{" "} - {t("discover.randomMovie.countdown", { countdown })} -

-
- )} -
- ); -} diff --git a/src/pages/discover/components/ScrollToTopButton.tsx b/src/pages/discover/components/ScrollToTopButton.tsx deleted file mode 100644 index 946109ae..00000000 --- a/src/pages/discover/components/ScrollToTopButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Icon, Icons } from "@/components/Icon"; - -export function ScrollToTopButton() { - const { t } = useTranslation(); - const [isVisible, setIsVisible] = useState(false); - - const toggleVisibility = () => { - const scrolled = window.scrollY > 300; - setIsVisible(scrolled); - }; - - useEffect(() => { - const handleScroll = () => { - const timeout = setTimeout(toggleVisibility, 100); - return () => clearTimeout(timeout); - }; - - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - }, []); - - const scrollToTop = () => { - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - - return ( -
-
- -
- ); -} diff --git a/src/pages/discover/discover.css b/src/pages/discover/discover.css deleted file mode 100644 index 2f2a9019..00000000 --- a/src/pages/discover/discover.css +++ /dev/null @@ -1,56 +0,0 @@ -.carousel-container { - position: relative; - mask-image: linear-gradient( - to right, - rgba(0, 0, 0, 0), /* Left edge */ - rgba(0, 0, 0, 1) 80px, /* visible after 80px */ - rgba(0, 0, 0, 1) calc(100% - 80px), /* invisible 80px from right */ - rgba(0, 0, 0, 0) 100% /* Right edge */ - ); - -webkit-mask-image: linear-gradient( - to right, - rgba(0, 0, 0, 0), - rgba(0, 0, 0, 1) 80px, - rgba(0, 0, 0, 1) calc(100% - 80px), - rgba(0, 0, 0, 0) 100% - ); - z-index: 1; -} - -@media (max-width: 768px) { - .carousel-container { - mask-image: linear-gradient( - to right, - rgba(0, 0, 0, 0), /* Left edge */ - rgba(0, 0, 0, 1) 20px, /* visible after 80px */ - rgba(0, 0, 0, 1) calc(100% - 20px), /* invisible 80px from right */ - rgba(0, 0, 0, 0) 100% /* Right edge */ - ); - -webkit-mask-image: linear-gradient( - to right, - rgba(0, 0, 0, 0), - rgba(0, 0, 0, 1) 20px, - rgba(0, 0, 0, 1) calc(100% - 20px), - rgba(0, 0, 0, 0) 100% - ); - } -} - -h2 { - position: relative; - z-index: 2; -} - -button { - position: relative; - z-index: 2; -} - -.scrollbar::-webkit-scrollbar { - display: none; -} - -.scrollbar { - scrollbar-width: none !important; - -ms-overflow-style: -ms-autohiding-scrollbar !important; -} diff --git a/src/pages/discover/discoverContent.tsx b/src/pages/discover/discoverContent.tsx deleted file mode 100644 index 1ffe4fe2..00000000 --- a/src/pages/discover/discoverContent.tsx +++ /dev/null @@ -1,526 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; - -import { get } from "@/backend/metadata/tmdb"; -import { DetailsModal } from "@/components/overlays/DetailsModal"; -import { useModal } from "@/components/overlays/Modal"; -import { useIsMobile } from "@/hooks/useIsMobile"; -import { - Genre, - Movie, - categories, - tvCategories, -} from "@/pages/discover/common"; -import { conf } from "@/setup/config"; -import { MediaItem } from "@/utils/mediaTypes"; - -import "./discover.css"; -import { CategoryButtons } from "./components/CategoryButtons"; -import { LazyMediaCarousel } from "./components/LazyMediaCarousel"; -import { LazyTabContent } from "./components/LazyTabContent"; -import { MediaCarousel } from "./components/MediaCarousel"; -import { RandomMovieButton } from "./components/RandomMovieButton"; -import { ScrollToTopButton } from "./components/ScrollToTopButton"; -import { useTMDBData } from "./hooks/useTMDBData"; - -const MOVIE_PROVIDERS = [ - { name: "Netflix", id: "8" }, - { name: "Apple TV+", id: "2" }, - { name: "Amazon Prime Video", id: "10" }, - { name: "Hulu", id: "15" }, - { name: "Max", id: "1899" }, - { name: "Paramount Plus", id: "531" }, - { name: "Disney Plus", id: "337" }, - { name: "Shudder", id: "99" }, -]; - -const TV_PROVIDERS = [ - { name: "Netflix", id: "8" }, - { name: "Apple TV+", id: "350" }, - { name: "Paramount Plus", id: "531" }, - { name: "Hulu", id: "15" }, - { name: "Max", id: "1899" }, - { name: "Disney Plus", id: "337" }, - { name: "fubuTV", id: "257" }, -]; - -// Editor Picks lists -const EDITOR_PICKS_MOVIES = [ - { id: 9342, type: "movie" }, // The Mask of Zorro - { id: 293, type: "movie" }, // A River Runs Through It - { id: 370172, type: "movie" }, // No Time To Die - { id: 661374, type: "movie" }, // The Glass Onion - { id: 207, type: "movie" }, // Dead Poets Society - { id: 378785, type: "movie" }, // The Best of the Blues Brothers - { id: 335984, type: "movie" }, // Blade Runner 2049 - { id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown - { id: 27205, type: "movie" }, // Inception - { id: 106646, type: "movie" }, // The Wolf of Wall Street - { id: 334533, type: "movie" }, // Captain Fantastic - { id: 693134, type: "movie" }, // Dune: Part Two - { id: 765245, type: "movie" }, // Swan Song - { id: 264660, type: "movie" }, // Ex Machina - { id: 92591, type: "movie" }, // Bernie - { id: 976893, type: "movie" }, // Perfect Days - { id: 13187, type: "movie" }, // A Charlie Brown Christmas - { id: 11527, type: "movie" }, // Excalibur - { id: 120, type: "movie" }, // LOTR: The Fellowship of the Ring - { id: 157336, type: "movie" }, // Interstellar - { id: 762, type: "movie" }, // Monty Python and the Holy Grail - { id: 666243, type: "movie" }, // The Witcher: Nightmare of the Wolf - { id: 545611, type: "movie" }, // Everything Everywhere All at Once - { id: 329, type: "movie" }, // Jurrassic Park - { id: 330459, type: "movie" }, // Rogue One: A Star Wars Story - { id: 279, type: "movie" }, // Amadeus - { id: 823219, type: "movie" }, // Flow - { id: 22, type: "movie" }, // Pirates of the Caribbean: The Curse of the Black Pearl - { id: 18971, type: "movie" }, // Rosencrantz and Guildenstern Are Dead - { id: 26388, type: "movie" }, // Buried - { id: 152601, type: "movie" }, // Her -]; - -const EDITOR_PICKS_TV_SHOWS = [ - { id: 456, type: "show" }, // The Simpsons - { id: 73021, type: "show" }, // Disenchantment - { id: 1434, type: "show" }, // Family Guy - { id: 1695, type: "show" }, // Monk - { id: 1408, type: "show" }, // House - { id: 93740, type: "show" }, // Foundation - { id: 60625, type: "show" }, // Rick and Morty - { id: 1396, type: "show" }, // Breaking Bad - { id: 44217, type: "show" }, // Vikings - { id: 90228, type: "show" }, // Dune Prophecy - { id: 13916, type: "show" }, // Death Note - { id: 71912, type: "show" }, // The Witcher - { id: 61222, type: "show" }, // Bojack Horseman - { id: 93405, type: "show" }, // Squid Game - { id: 87108, type: "show" }, // Chernobyl - { id: 105248, type: "show" }, // Cyberpunk: Edgerunners -]; - -export function DiscoverContent() { - // State management - const [selectedCategory, setSelectedCategory] = useState("movies"); - const [genres, setGenres] = useState([]); - const [tvGenres, setTVGenres] = useState([]); - const [randomMovie, setRandomMovie] = useState(null); - const [countdown, setCountdown] = useState(null); - const [countdownTimeout, setCountdownTimeout] = - useState(null); - const [selectedProvider, setSelectedProvider] = useState({ - name: "", - id: "", - }); - const [providerMovies, setProviderMovies] = useState([]); - const [providerTVShows, setProviderTVShows] = useState([]); - const [editorPicksMovies, setEditorPicksMovies] = useState([]); - const [editorPicksTVShows, setEditorPicksTVShows] = useState([]); - const [detailsData, setDetailsData] = useState(); - const detailsModal = useModal("discover-details"); - - // Refs - const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - - // Hooks - const navigate = useNavigate(); - const { isMobile } = useIsMobile(); - const { genreMedia: genreMovies } = useTMDBData(genres, categories, "movie"); - // const { genreMedia: genreTVShows } = useTMDBData( - // tvGenres, - // tvCategories, - // "tv", - // ); - const { t } = useTranslation(); - - // Only load data for the active tab - const isMoviesTab = selectedCategory === "movies"; - const isTVShowsTab = selectedCategory === "tvshows"; - const isEditorPicksTab = selectedCategory === "editorpicks"; - - // Fetch TV show genres - useEffect(() => { - if (!isTVShowsTab) return; - - const fetchTVGenres = async () => { - try { - const data = await get("/genre/tv/list", { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - }); - // Fetch only the first 10 TV show genres - setTVGenres(data.genres.slice(0, 10)); - } catch (error) { - console.error("Error fetching TV show genres:", error); - } - }; - - fetchTVGenres(); - }, [isTVShowsTab]); - - // Fetch Movie genres - useEffect(() => { - if (!isMoviesTab) return; - - const fetchGenres = async () => { - try { - const data = await get("/genre/movie/list", { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - }); - - // Fetch only the first 12 genres - setGenres(data.genres.slice(0, 12)); - } catch (error) { - console.error("Error fetching genres:", error); - } - }; - - fetchGenres(); - }, [isMoviesTab]); - - // Fetch Editor Picks Movies - useEffect(() => { - if (!isEditorPicksTab) return; - - const fetchEditorPicksMovies = async () => { - try { - const moviePromises = EDITOR_PICKS_MOVIES.map((item) => - get(`/movie/${item.id}`, { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - append_to_response: "videos,images", - }), - ); - - const results = await Promise.all(moviePromises); - // Shuffle the results to display them randomly - const shuffled = [...results].sort(() => 0.5 - Math.random()); - setEditorPicksMovies(shuffled); - } catch (error) { - console.error("Error fetching editor picks movies:", error); - } - }; - - fetchEditorPicksMovies(); - }, [isEditorPicksTab]); - - // Fetch Editor Picks TV Shows - useEffect(() => { - if (!isEditorPicksTab) return; - - const fetchEditorPicksTVShows = async () => { - try { - const tvShowPromises = EDITOR_PICKS_TV_SHOWS.map((item) => - get(`/tv/${item.id}`, { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - append_to_response: "videos,images", - }), - ); - - const results = await Promise.all(tvShowPromises); - // Shuffle the results to display them randomly - const shuffled = [...results].sort(() => 0.5 - Math.random()); - setEditorPicksTVShows(shuffled); - } catch (error) { - console.error("Error fetching editor picks TV shows:", error); - } - }; - - fetchEditorPicksTVShows(); - }, [isEditorPicksTab]); - - useEffect(() => { - let countdownInterval: NodeJS.Timeout; - if (countdown !== null && countdown > 0) { - countdownInterval = setInterval(() => { - setCountdown((prev) => (prev !== null ? prev - 1 : prev)); - }, 1000); - } - return () => clearInterval(countdownInterval); - }, [countdown]); - - // Handlers - const handleCategoryChange = ( - eventOrValue: React.ChangeEvent | string, - ) => { - const value = - typeof eventOrValue === "string" - ? eventOrValue - : eventOrValue.target.value; - setSelectedCategory(value); - }; - - const handleRandomMovieClick = () => { - const allMovies = Object.values(genreMovies).flat(); - const uniqueTitles = new Set(allMovies.map((movie) => movie.title)); - const uniqueTitlesArray = Array.from(uniqueTitles); - const randomIndex = Math.floor(Math.random() * uniqueTitlesArray.length); - const selectedMovie = allMovies.find( - (movie) => movie.title === uniqueTitlesArray[randomIndex], - ); - - if (selectedMovie) { - if (countdown !== null && countdown > 0) { - setCountdown(null); - if (countdownTimeout) { - clearTimeout(countdownTimeout); - setCountdownTimeout(null); - setRandomMovie(null); - } - } else { - setRandomMovie(selectedMovie as Movie); - setCountdown(5); - const timeoutId = setTimeout(() => { - navigate(`/media/tmdb-movie-${selectedMovie.id}-discover-random`); - }, 5000); - setCountdownTimeout(timeoutId); - } - } - }; - - const handleProviderClick = async (id: string, name: string) => { - try { - setSelectedProvider({ name, id }); - const endpoint = - selectedCategory === "movies" ? "/discover/movie" : "/discover/tv"; - const setData = - selectedCategory === "movies" ? setProviderMovies : setProviderTVShows; - const data = await get(endpoint, { - api_key: conf().TMDB_READ_API_KEY, - with_watch_providers: id, - watch_region: "US", - language: "en-US", - }); - setData(data.results); - } catch (error) { - console.error("Error fetching provider movies/shows:", error); - } - }; - - const handleCategoryClick = (id: string, name: string) => { - // Try both movie and tv versions of the category slug - const categorySlugBase = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); - const movieElement = document.getElementById( - `carousel-${categorySlugBase}-movie`, - ); - const tvElement = document.getElementById( - `carousel-${categorySlugBase}-tv`, - ); - - // Scroll to the first element that exists - const element = movieElement || tvElement; - if (element) { - element.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - }; - - const handleShowDetails = async (media: MediaItem) => { - setDetailsData({ - id: Number(media.id), - type: media.type === "movie" ? "movie" : "show", - }); - detailsModal.show(); - }; - - // Render Editor Picks content - const renderEditorPicksContent = () => { - return ( - <> - - - - ); - }; - - // Render Movies content with lazy loading - const renderMoviesContent = () => { - return ( - <> - {/* Provider Movies */} - {providerMovies.length > 0 && ( - - )} - - {/* Categories */} - {categories.map((category) => ( - - ))} - - {/* Genres */} - {genres.map((genre) => ( - - ))} - - ); - }; - - // Render TV Shows content with lazy loading - const renderTVShowsContent = () => { - return ( - <> - {/* Provider TV Shows */} - {providerTVShows.length > 0 && ( - - )} - - {/* Categories */} - {tvCategories.map((category) => ( - - ))} - - {/* Genres */} - {tvGenres.map((genre) => ( - - ))} - - ); - }; - - return ( -
- {/* Random Movie Button */} - - - {/* Category Tabs */} -
-
-
- {["movies", "tvshows", "editorpicks"].map((category) => ( - - ))} -
-
- - {/* Only show provider and genre buttons for movies and tvshows categories */} - {selectedCategory !== "editorpicks" && ( - <> -
- -
-
- -
- - )} -
- - {/* Content Section with Lazy Loading Tabs */} -
- {/* Movies Tab */} - - {renderMoviesContent()} - - - {/* TV Shows Tab */} - - {renderTVShowsContent()} - - - {/* Editor Picks Tab */} - - {renderEditorPicksContent()} - -
- - - - {detailsData && } -
- ); -} - -export default DiscoverContent; diff --git a/src/pages/discover/hooks/useIntersectionObserver.tsx b/src/pages/discover/hooks/useIntersectionObserver.tsx deleted file mode 100644 index 4a2c9aa0..00000000 --- a/src/pages/discover/hooks/useIntersectionObserver.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -interface IntersectionObserverOptions { - root?: Element | null; - rootMargin?: string; - threshold?: number | number[]; -} - -export function useIntersectionObserver( - options: IntersectionObserverOptions = {}, -) { - const [isIntersecting, setIsIntersecting] = useState(false); - const [hasIntersected, setHasIntersected] = useState(false); - const targetRef = useRef(null); - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - setIsIntersecting(entry.isIntersecting); - if (entry.isIntersecting) { - setHasIntersected(true); - } - }, - { - ...options, - rootMargin: options.rootMargin || "400px 0px", - }, - ); - - const currentTarget = targetRef.current; - if (currentTarget) { - observer.observe(currentTarget); - } - - return () => { - if (currentTarget) { - observer.unobserve(currentTarget); - } - }; - }, [options]); - - return { targetRef, isIntersecting, hasIntersected }; -} diff --git a/src/pages/discover/hooks/useTMDBData.tsx b/src/pages/discover/hooks/useTMDBData.tsx deleted file mode 100644 index c82e84b9..00000000 --- a/src/pages/discover/hooks/useTMDBData.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -import { get } from "@/backend/metadata/tmdb"; -import { Category, Genre, Movie, TVShow } from "@/pages/discover/common"; -import { conf } from "@/setup/config"; - -type MediaType = "movie" | "tv"; - -export function useTMDBData( - genres: Genre[], - categories: Category[], - mediaType: MediaType, - shouldLoad = true, -) { - const [genreMedia, setGenreMedia] = useState<{ - [id: number]: Movie[] | TVShow[]; - }>({}); - const [categoryMedia, setCategoryMedia] = useState<{ - [categoryName: string]: Movie[] | TVShow[]; - }>({}); - const [isLoading, setIsLoading] = useState(false); - - // Unified fetch function - const fetchMedia = useCallback( - async (endpoint: string, key: string, isGenre: boolean) => { - try { - const media: Movie[] | TVShow[] = []; - // Reduce the number of pages to improve performance - for (let page = 1; page <= 2; page += 1) { - const data = await get(endpoint, { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - page: page.toString(), - ...(isGenre ? { with_genres: key } : {}), - }); - media.push(...data.results); - } - - // Shuffle the media - for (let i = media.length - 1; i > 0; i -= 1) { - const j = Math.floor(Math.random() * (i + 1)); - [media[i], media[j]] = [media[j], media[i]]; - } - - return media; - } catch (error) { - console.error( - `Error fetching ${mediaType} for ${isGenre ? "genre" : "category"} ${key}:`, - error, - ); - return []; - } - }, - [mediaType], - ); - - // Fetch media for each genre - useEffect(() => { - if (!shouldLoad || genres.length === 0) return; - - const fetchMediaForGenres = async () => { - setIsLoading(true); - const genrePromises = genres.map(async (genre) => { - const media = await fetchMedia( - `/discover/${mediaType}`, - genre.id.toString(), - true, - ); - setGenreMedia((prev) => ({ ...prev, [genre.id]: media })); - }); - await Promise.all(genrePromises); - setIsLoading(false); - }; - - fetchMediaForGenres(); - }, [genres, mediaType, fetchMedia, shouldLoad]); - - // Fetch media for each category - useEffect(() => { - if (!shouldLoad || categories.length === 0) return; - - const fetchMediaForCategories = async () => { - setIsLoading(true); - const categoryPromises = categories.map(async (category) => { - const media = await fetchMedia(category.endpoint, category.name, false); - setCategoryMedia((prev) => ({ ...prev, [category.name]: media })); - }); - await Promise.all(categoryPromises); - setIsLoading(false); - }; - - fetchMediaForCategories(); - }, [categories, mediaType, fetchMedia, shouldLoad]); - - return { genreMedia, categoryMedia, isLoading }; -} - -// Create a hook for lazy loading a specific genre or category -export function useLazyTMDBData( - genre: Genre | null, - category: Category | null, - mediaType: MediaType, - shouldLoad = false, -) { - const [media, setMedia] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - const fetchMedia = useCallback( - async (endpoint: string, key: string, isGenre: boolean) => { - try { - setIsLoading(true); - const mediaItems: Movie[] | TVShow[] = []; - // Only fetch one page for better performance - const data = await get(endpoint, { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - page: "1", - ...(isGenre ? { with_genres: key } : {}), - }); - mediaItems.push(...data.results); - setMedia(mediaItems); - setIsLoading(false); - return mediaItems; - } catch (error) { - console.error( - `Error fetching ${mediaType} for ${isGenre ? "genre" : "category"}:`, - error, - ); - setIsLoading(false); - return []; - } - }, - [mediaType], - ); - - useEffect(() => { - if (!shouldLoad) return; - - if (genre) { - fetchMedia(`/discover/${mediaType}`, genre.id.toString(), true); - } else if (category) { - fetchMedia(category.endpoint, category.name, false); - } - }, [genre, category, mediaType, fetchMedia, shouldLoad]); - - return { media, isLoading }; -} diff --git a/src/pages/layouts/HomeLayout.tsx b/src/pages/layouts/HomeLayout.tsx index 9b50c9c1..008a780c 100644 --- a/src/pages/layouts/HomeLayout.tsx +++ b/src/pages/layouts/HomeLayout.tsx @@ -1,4 +1,3 @@ -import { FooterView } from "@/components/layout/Footer"; import { Navigation } from "@/components/layout/Navigation"; export function HomeLayout(props: { @@ -6,9 +5,9 @@ export function HomeLayout(props: { children: React.ReactNode; }) { return ( - +
{props.children} - +
); } diff --git a/src/pages/layouts/PageLayout.tsx b/src/pages/layouts/PageLayout.tsx index 01c2a017..6eb1941b 100644 --- a/src/pages/layouts/PageLayout.tsx +++ b/src/pages/layouts/PageLayout.tsx @@ -1,11 +1,10 @@ -import { FooterView } from "@/components/layout/Footer"; import { Navigation } from "@/components/layout/Navigation"; export function PageLayout(props: { children: React.ReactNode }) { return ( - + <> {props.children} - + ); } diff --git a/src/pages/layouts/SubPageLayout.tsx b/src/pages/layouts/SubPageLayout.tsx index c101ff58..eaeb9519 100644 --- a/src/pages/layouts/SubPageLayout.tsx +++ b/src/pages/layouts/SubPageLayout.tsx @@ -1,6 +1,5 @@ import classNames from "classnames"; -import { FooterView } from "@/components/layout/Footer"; import { Navigation } from "@/components/layout/Navigation"; export function BlurEllipsis(props: { positionClass?: string }) { @@ -34,10 +33,8 @@ export function SubPageLayout(props: { children: React.ReactNode }) { > {/* Main page */} - - -
{props.children}
-
+ +
{props.children}
); } diff --git a/src/pages/parts/auth/LoginFormPart.tsx b/src/pages/parts/auth/LoginFormPart.tsx index ac63fad2..86bd7c22 100644 --- a/src/pages/parts/auth/LoginFormPart.tsx +++ b/src/pages/parts/auth/LoginFormPart.tsx @@ -65,7 +65,7 @@ export function LoginFormPart(props: LoginFormPartProps) { ); return ( - }> + }> {t("auth.login.description")} diff --git a/src/pages/parts/home/BookmarksPart.tsx b/src/pages/parts/home/BookmarksPart.tsx index bb1e67c5..247f99d0 100644 --- a/src/pages/parts/home/BookmarksPart.tsx +++ b/src/pages/parts/home/BookmarksPart.tsx @@ -15,10 +15,8 @@ const LONG_PRESS_DURATION = 500; // 0.5 seconds export function BookmarksPart({ onItemsChange, - onShowDetails, }: { onItemsChange: (hasItems: boolean) => void; - onShowDetails?: (media: MediaItem) => void; }) { const { t } = useTranslation(); const progressItems = useProgressStore((s) => s.items); @@ -118,7 +116,6 @@ export function BookmarksPart({ media={v} closable={editing} onClose={() => removeBookmark(v.id)} - onShowDetails={onShowDetails} />
))} diff --git a/src/pages/parts/home/HeroPart.tsx b/src/pages/parts/home/HeroPart.tsx index 56717a67..6ed1ef7a 100644 --- a/src/pages/parts/home/HeroPart.tsx +++ b/src/pages/parts/home/HeroPart.tsx @@ -7,7 +7,6 @@ import { Icon, Icons } from "@/components/Icon"; import { ThinContainer } from "@/components/layout/ThinContainer"; import { useSlashFocus } from "@/components/player/hooks/useSlashFocus"; import { HeroTitle } from "@/components/text/HeroTitle"; -import { useIsTV } from "@/hooks/useIsTv"; import { useRandomTranslation } from "@/hooks/useRandomTranslation"; import { useSearchQuery } from "@/hooks/useSearchQuery"; import { conf } from "@/setup/config"; @@ -65,8 +64,6 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { ); const { width: windowWidth, height: windowHeight } = useWindowSize(); - const { isTV } = useIsTV(); - // Detect if running as a PWA on iOS const isIOSPWA = /iPad|iPhone|iPod/i.test(navigator.userAgent) && @@ -105,9 +102,9 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
- {isTV && search.length > 0 ? null : ( - {title} - )} + + What do you want to watch? +
void; - onShowDetails?: (media: MediaItem) => void; -}) { + className?: string; +} + +export function WatchingPart({ onItemsChange, className }: WatchingPartProps) { const { t } = useTranslation(); const progressItems = useProgressStore((s) => s.items); const removeItem = useProgressStore((s) => s.removeItem); @@ -82,7 +81,7 @@ export function WatchingPart({ if (sortedProgressItems.length === 0) return null; return ( -
+
{sortedProgressItems.map((v) => (
) => e.preventDefault() - } - onTouchStart={handleTouchStart} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} + } // Prevent right-click context menu + onTouchStart={handleTouchStart} // Handle touch start + onTouchEnd={handleTouchEnd} // Handle touch end + onMouseDown={handleMouseDown} // Handle mouse down + onMouseUp={handleMouseUp} // Handle mouse up > removeItem(v.id)} - onShowDetails={onShowDetails} />
))} diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index 5e254332..11b1c640 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -4,7 +4,6 @@ import IosPwaLimitations from "@/components/buttons/IosPwaLimitations"; import { BrandPill } from "@/components/layout/BrandPill"; import { Player } from "@/components/player"; import { SkipIntroButton } from "@/components/player/atoms/SkipIntroButton"; -import { UnreleasedEpisodeOverlay } from "@/components/player/atoms/UnreleasedEpisodeOverlay"; import { Widescreen } from "@/components/player/atoms/Widescreen"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { useSkipTime } from "@/components/player/hooks/useSkipTime"; @@ -12,7 +11,7 @@ import { useIsMobile } from "@/hooks/useIsMobile"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; -import { ScrapingPartInterruptButton, Tips } from "./ScrapingPart"; +import { Tips } from "./ScrapingPart"; export interface PlayerPartProps { children?: ReactNode; @@ -89,8 +88,6 @@ export function PlayerPart(props: PlayerPartProps) { / - -
@@ -112,10 +109,7 @@ export function PlayerPart(props: PlayerPartProps) { {status === playerStatus.PLAYING ? null : } -
- {status === playerStatus.SCRAPING ? ( - - ) : null} +
{status === playerStatus.PLAYING ? ( <> {isMobile ? : null} @@ -123,18 +117,19 @@ export function PlayerPart(props: PlayerPartProps) { ) : null}
-
+
{status === playerStatus.PLAYING ? ( - <> +
- + - +
) : null}
+
{status === playerStatus.PLAYING ? ( @@ -144,15 +139,10 @@ export function PlayerPart(props: PlayerPartProps) { ) : null} - {status === playerStatus.PLAYBACK_ERROR || - status === playerStatus.PLAYING ? ( - - ) : null} + {(status === playerStatus.PLAYBACK_ERROR || + status === playerStatus.PLAYING) && } - {/* Fullscreen on when not shifting */} {!isShifting && } - - {/* Expand button visible when shifting */} {isShifting && (
@@ -160,31 +150,31 @@ export function PlayerPart(props: PlayerPartProps) { )}
-
+
- {/* Disable PiP for iOS PWA */} {!isIOSPWA && status === playerStatus.PLAYING && } - {status === playerStatus.PLAYING ? ( -
- -
- ) : null} - {isIOSPWA && } + {isIOSPWA && status === playerStatus.PLAYING && }
- {/* iOS PWA */} - {!isIOSPWA && } - {isIOSPWA && status === playerStatus.PLAYING && } + {!isIOSPWA && ( +
+ +
+ )} + {isIOSPWA && ( +
+ +
+ )}
- void; } +interface ScrapePillProps { + name: string; + status: string; + percentage: number; +} + +function ScrapePillSkeleton() { + return ( +
+ ); +} + +function ScrapePill({ name, status, percentage }: ScrapePillProps) { + const isError = status === "failure"; + + return ( +
+
+ {!isError ? ( + + ) : ( + + )} +
+
+

+ {name} +

+
+
+ ); +} + export function ScrapingPart(props: ScrapingProps) { const { report } = useReportProviders(); const { startScraping, sourceOrder, sources, currentSource } = useScrape(); const isMounted = useMountedState(); const { t } = useTranslation(); - const containerRef = useRef(null); - const listRef = useRef(null); const [failedStartScrape, setFailedStartScrape] = useState(false); - const renderedOnce = useListCenter( - containerRef, - listRef, - sourceOrder, - currentSource, - ); const resultRef = useRef({ sourceOrder, @@ -92,91 +124,56 @@ export function ScrapingPart(props: ScrapingProps) { return {t("player.turnstile.error")}; return ( -
+
{!sourceOrder || sourceOrder.length === 0 ? ( -
+

{t("player.turnstile.verifyingHumanity")}

- ) : null} -
- {sourceOrder.map((order) => { - const source = sources[order.id]; - const distance = Math.abs( - sourceOrder.findIndex((o) => o.id === order.id) - - currentProviderIndex, - ); - return ( + ) : ( +
+
+ +

+ Finding the best video for you +

+
+
- 0} - percentage={source.percentage} - > +
0, - })} + className="absolute inset-y-0 left-0 flex items-center gap-[16px] transition-transform duration-200" + style={{ + transform: `translateX(${ + -1 * (220 + 16) * (currentProviderIndex + 1) + }px)`, + }} > - {order.children.map((embedId) => { - const embed = sources[embedId]; + + {sourceOrder.map((order) => { + const source = sources[order.id]; return ( - ); })} +
- +
- ); - })} -
-
- ); -} - -export function ScrapingPartInterruptButton() { - const { t } = useTranslation(); - - return ( -
- - +
+
+ )}
); } diff --git a/src/pages/parts/search/SearchListPart.tsx b/src/pages/parts/search/SearchListPart.tsx index b5f29c13..f8154dd0 100644 --- a/src/pages/parts/search/SearchListPart.tsx +++ b/src/pages/parts/search/SearchListPart.tsx @@ -57,13 +57,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) { ); } -export function SearchListPart({ - searchQuery, - onShowDetails, -}: { - searchQuery: string; - onShowDetails?: (media: MediaItem) => void; -}) { +export function SearchListPart({ searchQuery }: { searchQuery: string }) { const { t } = useTranslation(); const [results, setResults] = useState([]); @@ -93,11 +87,7 @@ export function SearchListPart({ /> {results.map((v) => ( - + ))}
diff --git a/src/pages/parts/settings/AppearancePart.tsx b/src/pages/parts/settings/AppearancePart.tsx index 1c364c68..cb053995 100644 --- a/src/pages/parts/settings/AppearancePart.tsx +++ b/src/pages/parts/settings/AppearancePart.tsx @@ -11,46 +11,6 @@ const availableThemes = [ selector: "theme-default", key: "settings.appearance.themes.default", }, - { - id: "classic", - selector: "theme-classic", - key: "settings.appearance.themes.classic", - }, - { - id: "blue", - selector: "theme-blue", - key: "settings.appearance.themes.blue", - }, - { - id: "teal", - selector: "theme-teal", - key: "settings.appearance.themes.teal", - }, - { - id: "red", - selector: "theme-red", - key: "settings.appearance.themes.red", - }, - { - id: "gray", - selector: "theme-gray", - key: "settings.appearance.themes.gray", - }, - { - id: "green", - selector: "theme-green", - key: "settings.appearance.themes.green", - }, - { - id: "mocha", - selector: "theme-mocha", - key: "settings.appearance.themes.mocha", - }, - { - id: "pink", - selector: "theme-pink", - key: "settings.appearance.themes.pink", - }, ]; function ThemePreview(props: { diff --git a/src/pages/parts/settings/SidebarPart.tsx b/src/pages/parts/settings/SidebarPart.tsx index 98404c84..255fcbd8 100644 --- a/src/pages/parts/settings/SidebarPart.tsx +++ b/src/pages/parts/settings/SidebarPart.tsx @@ -6,6 +6,7 @@ import { useAsync } from "react-use"; import { getBackendMeta } from "@/backend/accounts/meta"; import { Icon, Icons } from "@/components/Icon"; import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar"; +import { GlitchText } from "@/components/text/GlitchText"; import { Divider } from "@/components/utils/Divider"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useIsMobile } from "@/hooks/useIsMobile"; @@ -48,11 +49,6 @@ export function SidebarPart() { id: "settings-preferences", icon: Icons.SETTINGS, }, - { - textKey: "settings.appearance.title", - id: "settings-appearance", - icon: Icons.BRUSH, - }, { textKey: "settings.subtitles.title", id: "settings-captions", @@ -120,7 +116,7 @@ export function SidebarPart() {
-
+
{/* Hostname */}

@@ -179,9 +178,14 @@ export function SidebarPart() {

{t("settings.sidebar.info.appVersion")}

-

- {conf().APP_VERSION} -

+
+ +
{/* Backend version */} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index bbfa9bfd..ec210d07 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -15,7 +15,6 @@ import { useOnlineListener } from "@/hooks/usePing"; import { AboutPage } from "@/pages/About"; import { AdminPage } from "@/pages/admin/AdminPage"; import VideoTesterView from "@/pages/developer/VideoTesterView"; -import { Discover } from "@/pages/discover/Discover"; import { DmcaPage } from "@/pages/Dmca"; import MaintenancePage from "@/pages/errors/MaintenancePage"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; @@ -166,8 +165,6 @@ function App() { {/* Support page */} } /> } /> - {/* Discover page */} - } /> {/* Settings page */} } /> {/* Support page */} } /> - {/* Discover page */} - } /> {/* Settings page */} { addVariant("dir-neutral", "[dir] &"); diff --git a/themes/all.ts b/themes/all.ts index d42e1990..15c339f9 100644 --- a/themes/all.ts +++ b/themes/all.ts @@ -1,19 +1,5 @@ -import teal from "./list/teal"; -import blue from "./list/blue"; -import red from "./list/red"; -import gray from "./list/gray"; import classic from "./list/classic"; -import green from "./list/green"; -import mocha from "./list/mocha"; -import pink from "./list/pink"; export const allThemes = [ - teal, - blue, - gray, - red, - classic, - green, - mocha, - pink + classic ] diff --git a/themes/default.ts b/themes/default.ts index 7ae3c097..5ce822e6 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -1,348 +1,292 @@ const tokens = { - black: { - c50: "#000000", - c75: "#030303", - c80: "#080808", - c100: "#0d0d0d", - c125: "#141414", - c150: "#1a1a1a", - c200: "#262626", - c250: "#333333" - }, - white: "#FFFFFF", // General white color + black: "#000000", + white: "#FFFFFF", semantic: { red: { - c100: "#F46E6E", // Error text - c200: "#E44F4F", // Video player scraping error - c300: "#D74747", // Danger button - c400: "#B43434", // Not currently used + c100: "#CD97D6", // Using bink-700 for errors since we don't have red + c200: "#A87FD1", // Using bink-600 for error states + c300: "#8D66B5", // Using bink-500 for danger buttons }, green: { - c100: "#60D26A", // Success text - c200: "#40B44B", // Video player scraping success - c300: "#31A33C", // Not currently used - c400: "#237A2B", // Not currently used + c100: "#A87FD1", // Using bink-600 for success states + c200: "#8D66B5", // Using bink-500 for success indicators + c300: "#714C97", // Using bink-400 for success buttons }, silver: { - c100: "#DEDEDE", // Primary button hover - c200: "#B6CAD7", // Not currently used - c300: "#8EA3B0", // Secondary button text - c400: "#617A8A", // Main text in video player context - }, - yellow: { - c100: "#FFF599", // Best onboarding highlight - c200: "#FCEC61", // Dropdown highlight hover - c300: "#D8C947", // Not currently used - c400: "#AFA349", // Dropdown highlight - }, - rose: { - c100: "#DB3D61", // Authentication error text - c200: "#8A293B", // Danger button hover - c300: "#812435", // Danger button - c400: "#701B2B", // Not currently used - }, + c100: "#7A758F", // Using denim-700 for hover states + c300: "#504B64", // Using denim-600 for secondary text + c400: "#38334A", // Using denim-500 for dimmed text + } }, - blue: { - c50: "#ccccd6", - c100: "#a2a2a2", - c200: "#868686", - c300: "#646464", - c400: "#4e4e4e", - c500: "#383838", - c600: "#2e2e2e", - c700: "#272727", - c800: "#181818", - c900: "#0f0f0f" + // Simplified color palette using new theme colors + primary: { + c100: "#CD97D6", // bink-700 + c200: "#A87FD1", // bink-600 + c300: "#8D66B5", // bink-500 + c400: "#714C97", // bink-400 + c500: "#533670", // bink-300 + c600: "#412B57", // bink-200 + c700: "#432449", // bink-100 }, - purple: { - c50: "#aaafff", - c100: "#8288fe", - c200: "#5a62eb", - c300: "#454cd4", - c400: "#333abe", - c500: "#292d86", - c600: "#1f2363", - c700: "#191b4a", - c800: "#111334", // Lightbar - c900: "#0b0d22" + background: { + c100: "#7A758F", // denim-700 + c200: "#504B64", // denim-600 + c300: "#38334A", // denim-500 + c400: "#2B263D", // denim-400 + c500: "#211D30", // denim-300 + c600: "#191526", // denim-200 + c700: "#120F1D", // denim-100 }, ash: { - c50: "#8d8d8d", - c100: "#6b6b6b", - c200: "#545454", - c300: "#3c3c3c", - c400: "#313131", - c500: "#2c2c2c", - c600: "#252525", - c700: "#1e1e1e", - c800: "#181818", - c900: "#111111" - }, - shade: { - c25: "#939393", // Media card hover accent - c50: "#7c7c7c", - c100: "#666666", - c200: "#4f4f4f", - c300: "#404040", - c400: "#343434", - c500: "#282828", - c600: "#202020", - c700: "#1a1a1a", - c800: "#151515", - c900: "#0e0e0e" - }, -}; + c100: "#1E1C26", // ash-100 + c200: "#2B2836", // ash-200 + c300: "#2C293A", // ash-300 + c400: "#3D394D", // ash-400 + c500: "#9C93B5", // ash-500 + c600: "#817998", // ash-600 + } +} export const defaultTheme = { extend: { colors: { themePreview: { - primary: tokens.black.c80, - secondary: tokens.black.c100, + primary: tokens.primary.c300, + secondary: tokens.background.c200, ghost: tokens.white, }, // Branding pill: { - background: tokens.black.c100, - backgroundHover: tokens.black.c125, - highlight: tokens.blue.c200, - activeBackground: tokens.shade.c700, + background: tokens.background.c400, + backgroundHover: tokens.background.c300, + highlight: tokens.primary.c300, + activeBackground: tokens.background.c400, }, - // meta data for the theme itself global: { - accentA: tokens.blue.c200, - accentB: tokens.blue.c300, + accentA: tokens.primary.c300, + accentB: tokens.primary.c400, }, - // light bar lightBar: { - light: tokens.purple.c800, + light: tokens.primary.c400, }, - // Buttons buttons: { - toggle: tokens.purple.c300, - toggleDisabled: tokens.black.c200, - danger: tokens.semantic.rose.c300, - dangerHover: tokens.semantic.rose.c200, - - secondary: tokens.black.c100, + toggle: tokens.primary.c300, + toggleDisabled: tokens.background.c400, + danger: tokens.semantic.red.c300, + dangerHover: tokens.semantic.red.c200, + secondary: tokens.background.c500, secondaryText: tokens.semantic.silver.c300, - secondaryHover: tokens.black.c150, + secondaryHover: tokens.background.c400, primary: tokens.white, - primaryText: tokens.black.c50, + primaryText: tokens.black, primaryHover: tokens.semantic.silver.c100, - purple: tokens.purple.c600, - purpleHover: tokens.purple.c400, - cancel: tokens.black.c100, - cancelHover: tokens.black.c150 + cancel: tokens.background.c400, + cancelHover: tokens.background.c300, }, - // only used for body colors/textures background: { - main: tokens.black.c75, - secondary: tokens.black.c75, - secondaryHover: tokens.black.c75, - accentA: tokens.purple.c600, - accentB: tokens.black.c100 + main: tokens.background.c700, + secondary: tokens.background.c600, + secondaryHover: tokens.background.c500, + accentA: tokens.primary.c600, + accentB: tokens.primary.c500, }, - // Modals modal: { - background: tokens.shade.c800, + background: tokens.background.c600, }, - // typography type: { - logo: tokens.purple.c100, + logo: tokens.primary.c200, emphasis: tokens.white, - text: tokens.shade.c50, - dimmed: tokens.shade.c50, - divider: tokens.ash.c500, - secondary: tokens.ash.c100, + text: tokens.background.c100, + dimmed: tokens.background.c200, + divider: tokens.background.c400, + secondary: tokens.semantic.silver.c300, danger: tokens.semantic.red.c100, success: tokens.semantic.green.c100, - link: tokens.purple.c100, - linkHover: tokens.purple.c50 + link: tokens.primary.c200, + linkHover: tokens.primary.c100, }, // search bar search: { - background: tokens.black.c100, - hoverBackground: tokens.shade.c900, - focused: tokens.black.c125, - placeholder: tokens.shade.c200, - icon: tokens.shade.c500, + background: tokens.background.c500, + hoverBackground: tokens.background.c600, + focused: tokens.background.c400, + placeholder: tokens.background.c100, + icon: tokens.background.c100, text: tokens.white, }, // media cards mediaCard: { - hoverBackground: tokens.shade.c900, - hoverAccent: tokens.black.c250, - hoverShadow: tokens.black.c50, - shadow: tokens.shade.c800, + hoverBackground: tokens.background.c600, + hoverAccent: tokens.primary.c100, + hoverShadow: tokens.background.c700, + shadow: tokens.background.c500, barColor: tokens.ash.c200, - barFillColor: tokens.purple.c100, - badge: tokens.shade.c700, - badgeText: tokens.ash.c100 + barFillColor: tokens.primary.c200, + badge: tokens.background.c500, + badgeText: tokens.ash.c500, }, // Large card largeCard: { - background: tokens.black.c100, - icon: tokens.purple.c400, + background: tokens.background.c600, + icon: tokens.primary.c400, }, // Dropdown dropdown: { - background: tokens.black.c100, - altBackground: tokens.black.c80, - hoverBackground: tokens.black.c150, - highlight: tokens.semantic.yellow.c400, - highlightHover: tokens.semantic.yellow.c200, - text: tokens.shade.c50, - secondary: tokens.shade.c100, - border: tokens.shade.c400, - contentBackground: tokens.black.c50 + background: tokens.background.c600, + altBackground: tokens.background.c500, + hoverBackground: tokens.background.c400, + highlight: tokens.primary.c300, + highlightHover: tokens.primary.c200, + text: tokens.background.c100, + secondary: tokens.background.c200, + border: tokens.background.c400, + contentBackground: tokens.background.c500, }, // Passphrase authentication: { - border: tokens.shade.c300, - inputBg: tokens.black.c100, - inputBgHover: tokens.black.c150, - wordBackground: tokens.shade.c500, - copyText: tokens.shade.c100, - copyTextHover: tokens.ash.c50, - errorText: tokens.semantic.rose.c100, + border: tokens.background.c400, + inputBg: tokens.background.c600, + inputBgHover: tokens.background.c500, + wordBackground: tokens.background.c500, + copyText: tokens.background.c200, + copyTextHover: tokens.ash.c500, + errorText: tokens.semantic.red.c100, }, // Settings page settings: { sidebar: { - activeLink: tokens.black.c100, - badge: tokens.shade.c900, + activeLink: tokens.background.c600, + badge: tokens.background.c700, type: { - secondary: tokens.shade.c200, - inactive: tokens.shade.c50, - icon: tokens.black.c200, - iconActivated: tokens.purple.c200, - activated: tokens.purple.c100 + secondary: tokens.background.c300, + inactive: tokens.background.c100, + icon: tokens.background.c100, + iconActivated: tokens.primary.c200, + activated: tokens.primary.c100, }, }, card: { - border: tokens.shade.c700, - background: tokens.black.c100, - altBackground: tokens.black.c100 + border: tokens.background.c400, + background: tokens.background.c400, + altBackground: tokens.background.c400, }, saveBar: { - background: tokens.black.c50 + background: tokens.background.c600, }, }, // Utilities utils: { - divider: tokens.ash.c300 + divider: tokens.ash.c300, }, // Onboarding onboarding: { - bar: tokens.shade.c400, - barFilled: tokens.purple.c300, - divider: tokens.shade.c200, - card: tokens.shade.c800, - cardHover: tokens.shade.c700, - border: tokens.shade.c600, - good: tokens.purple.c100, - best: tokens.semantic.yellow.c100, - link: tokens.purple.c100, + bar: tokens.background.c400, + barFilled: tokens.primary.c300, + divider: tokens.background.c300, + card: tokens.background.c600, + cardHover: tokens.background.c500, + border: tokens.background.c600, + good: tokens.primary.c200, + best: tokens.primary.c100, + link: tokens.primary.c200, }, // Error page errors: { - card: tokens.black.c75, + card: tokens.background.c600, border: tokens.ash.c500, type: { - secondary: tokens.ash.c100, + secondary: tokens.ash.c500, }, }, // About page about: { - circle: tokens.black.c100, - circleText: tokens.ash.c50 + circle: tokens.ash.c500, + circleText: tokens.ash.c500, }, - // About page editBadge: { bg: tokens.ash.c500, bgHover: tokens.ash.c400, - text: tokens.ash.c50 + text: tokens.ash.c500, }, progress: { - background: tokens.ash.c50, - preloaded: tokens.ash.c50, - filled: tokens.purple.c200, + background: tokens.ash.c500, + preloaded: tokens.ash.c500, + filled: tokens.primary.c200, }, // video player video: { - buttonBackground: tokens.ash.c600, + buttonBackground: tokens.ash.c200, autoPlay: { - background: tokens.ash.c800, - hover: tokens.ash.c600, + background: tokens.ash.c600, + hover: tokens.ash.c500, }, scraping: { - card: tokens.black.c50, + card: tokens.background.c500, error: tokens.semantic.red.c200, success: tokens.semantic.green.c200, - loading: tokens.purple.c200, - noresult: tokens.black.c200 + loading: tokens.primary.c200, + noresult: tokens.ash.c500, }, audio: { - set: tokens.purple.c200, + set: tokens.primary.c200, }, context: { - background: tokens.black.c50, - light: tokens.shade.c50, - border: tokens.ash.c600, - hoverColor: tokens.ash.c600, - buttonFocus: tokens.ash.c500, - flagBg: tokens.ash.c500, - inputBg: tokens.black.c100, - buttonOverInputHover: tokens.ash.c500, - inputPlaceholder: tokens.ash.c200, - cardBorder: tokens.ash.c700, - slider: tokens.black.c200, - sliderFilled: tokens.purple.c200, + background: tokens.background.c600, + light: tokens.background.c400, + border: tokens.background.c500, + hoverColor: tokens.background.c500, + buttonFocus: tokens.background.c400, + flagBg: tokens.background.c400, + inputBg: tokens.background.c500, + buttonOverInputHover: tokens.background.c400, + inputPlaceholder: tokens.background.c200, + cardBorder: tokens.background.c600, + slider: tokens.background.c100, + sliderFilled: tokens.primary.c300, error: tokens.semantic.red.c200, buttons: { - list: tokens.ash.c700, - active: tokens.ash.c900, + list: tokens.background.c600, + active: tokens.background.c600, }, - closeHover: tokens.ash.c800, + closeHover: tokens.background.c500, type: { - main: tokens.semantic.silver.c300, - secondary: tokens.ash.c200, - accent: tokens.purple.c200, + main: tokens.white, + secondary: tokens.background.c200, + accent: tokens.primary.c300, }, }, }, }, }, -}; +}