diff --git a/index.html b/index.html index 6ed209eb..b497e9ec 100644 --- a/index.html +++ b/index.html @@ -18,9 +18,11 @@ - + + + diff --git a/src/assets/css/index.css b/src/assets/css/index.css index 092499bc..557656b4 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -1,9 +1,11 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; html, body { + font-family: "Lato", sans-serif !important; @apply bg-background-main font-main text-type-text; min-height: 100vh; min-height: 100dvh; @@ -79,6 +81,16 @@ html[data-no-scroll], html[data-no-scroll] body { min-height: 100dvh; } +.info-button { + display: inline-block; + padding: 0.75em; + margin: -0.75em; + position: absolute; + bottom: 0; + right: 0; + transform: translate(-15px, -10px) +} + /*generated with Input range slider CSS style generator (version 20211225) https://toughengineer.github.io/demo/slider-styler*/ :root { diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx index c3039436..42908063 100644 --- a/src/components/buttons/EditButton.tsx +++ b/src/components/buttons/EditButton.tsx @@ -1,5 +1,5 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useCallback } from "react"; +import { useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Icon, Icons } from "@/components/Icon"; @@ -7,31 +7,48 @@ import { Icon, Icons } from "@/components/Icon"; export interface EditButtonProps { editing: boolean; onEdit?: (editing: boolean) => void; + id?: string; } export function EditButton(props: EditButtonProps) { const { t } = useTranslation(); const [parent] = useAutoAnimate(); + const buttonRef = useRef(null); const onClick = useCallback(() => { props.onEdit?.(!props.editing); }, [props]); return ( - + <> + + + {props.editing && ( + + )} + ); } diff --git a/src/components/layout/WideContainer.tsx b/src/components/layout/WideContainer.tsx index bcccd5e5..c5cb49ad 100644 --- a/src/components/layout/WideContainer.tsx +++ b/src/components/layout/WideContainer.tsx @@ -10,7 +10,9 @@ export function WideContainer(props: WideContainerProps) { return (
{props.children} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index edf10769..22ed8500 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -12,7 +12,7 @@ import { MediaItem } from "@/utils/mediaTypes"; import { MediaBookmarkButton } from "./MediaBookmark"; import { IconPatch } from "../buttons/IconPatch"; -import { Icons } from "../Icon"; +import { Icon, Icons } from "../Icon"; export interface MediaCardProps { media: MediaItem; @@ -179,7 +179,29 @@ function MediaCardContent({

{media.title}

- +
+ + +
); diff --git a/src/components/media/MediaGrid.tsx b/src/components/media/MediaGrid.tsx index adb9b932..fad039a1 100644 --- a/src/components/media/MediaGrid.tsx +++ b/src/components/media/MediaGrid.tsx @@ -8,7 +8,7 @@ export const MediaGrid = forwardRef( (props, ref) => { return (
{props.children} diff --git a/src/pages/Discover.tsx b/src/pages/Discover.tsx index 2f34c46b..3091f882 100644 --- a/src/pages/Discover.tsx +++ b/src/pages/Discover.tsx @@ -23,6 +23,25 @@ import { SubPageLayout } from "./layouts/SubPageLayout"; import { Icon, Icons } from "../components/Icon"; import { PageTitle } from "./parts/util/PageTitle"; +const editorPicks = [ + { 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 +]; + export function Discover() { const { t } = useTranslation(); const [genres, setGenres] = useState([]); @@ -48,6 +67,45 @@ export function Discover() { const [countdownTimeout, setCountdownTimeout] = useState(null); + const [editorPicksData, setEditorPicksData] = useState([]); + + useEffect(() => { + // Function to shuffle array + const shuffleArray = (array: any[]) => { + for (let i = array.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + }; + + const fetchEditorPicks = async () => { + try { + // Shuffle the editorPicks array + const shuffledPicks = shuffleArray([...editorPicks]); + + const promises = shuffledPicks.map(async (pick) => { + const endpoint = + pick.type === "movie" ? `/movie/${pick.id}` : `/tv/${pick.id}`; + const data = await get(endpoint, { + api_key: conf().TMDB_READ_API_KEY, + language: "en-US", + }); + return { + ...data, + type: pick.type, + }; + }); + const results = await Promise.all(promises); + setEditorPicksData(results); + } catch (error) { + console.error("Error fetching editor picks:", error); + } + }; + + fetchEditorPicks(); + }, []); + useEffect(() => { const fetchMoviesForCategory = async (category: Category) => { try { @@ -315,11 +373,13 @@ export function Discover() { const displayCategory = category === "Now Playing" ? "In Cinemas" - : category.includes("Movie") - ? `${category}s` - : isTVShow - ? `${category} Shows` - : `${category} Movies`; + : category === "Editor Picks" // Check for "Editor Picks" specifically + ? category + : category.includes("Movie") + ? `${category}s` + : isTVShow + ? `${category} Shows` + : `${category} Movies`; // https://tailwindcss.com/docs/border-style return ( @@ -532,6 +592,16 @@ export function Discover() {

)} + + {/* Editor Picks Section */} +
+ {editorPicksData.length > 0 && ( +
+ {renderMovies(editorPicksData, "Editor Picks")} +
+ )} +
+
{categories.map((category) => (
- +
{!(showBookmarks || showWatching) ? (
diff --git a/src/pages/parts/home/BookmarksPart.tsx b/src/pages/parts/home/BookmarksPart.tsx index 4f9f13bc..3ffc9674 100644 --- a/src/pages/parts/home/BookmarksPart.tsx +++ b/src/pages/parts/home/BookmarksPart.tsx @@ -1,5 +1,5 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { EditButton } from "@/components/buttons/EditButton"; @@ -11,6 +11,8 @@ import { useBookmarkStore } from "@/stores/bookmarks"; import { useProgressStore } from "@/stores/progress"; import { MediaItem } from "@/utils/mediaTypes"; +const LONG_PRESS_DURATION = 500; // 0.5 seconds + export function BookmarksPart({ onItemsChange, }: { @@ -23,6 +25,8 @@ export function BookmarksPart({ const [editing, setEditing] = useState(false); const [gridRef] = useAutoAnimate(); + const pressTimerRef = useRef(null); + const items = useMemo(() => { let output: MediaItem[] = []; Object.entries(bookmarks).forEach((entry) => { @@ -49,15 +53,60 @@ export function BookmarksPart({ onItemsChange(items.length > 0); }, [items, onItemsChange]); + const handleLongPress = () => { + // Find the button by ID and simulate a click + const editButton = document.getElementById("edit-button-bookmark"); + if (editButton) { + (editButton as HTMLButtonElement).click(); + } + }; + + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); // Prevent default touch action + pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION); + }; + + const handleTouchEnd = () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + pressTimerRef.current = null; + } + }; + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); // Prevent default mouse action + pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION); + }; + + const handleMouseUp = () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + pressTimerRef.current = null; + } + }; + if (items.length === 0) return null; return ( -
+
) => + e.preventDefault() + } // 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 + > - + {items.map((v) => ( diff --git a/src/pages/parts/home/WatchingPart.tsx b/src/pages/parts/home/WatchingPart.tsx index 80e7e09c..94f2ac12 100644 --- a/src/pages/parts/home/WatchingPart.tsx +++ b/src/pages/parts/home/WatchingPart.tsx @@ -1,5 +1,5 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { EditButton } from "@/components/buttons/EditButton"; @@ -7,25 +7,27 @@ import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; -import { useBookmarkStore } from "@/stores/bookmarks"; import { useProgressStore } from "@/stores/progress"; import { shouldShowProgress } from "@/stores/progress/utils"; import { MediaItem } from "@/utils/mediaTypes"; +const LONG_PRESS_DURATION = 500; // 0.5 seconds + export function WatchingPart({ onItemsChange, }: { onItemsChange: (hasItems: boolean) => void; }) { const { t } = useTranslation(); - const bookmarks = useBookmarkStore((s) => s.bookmarks); const progressItems = useProgressStore((s) => s.items); const removeItem = useProgressStore((s) => s.removeItem); const [editing, setEditing] = useState(false); const [gridRef] = useAutoAnimate(); + const pressTimerRef = useRef(null); + const sortedProgressItems = useMemo(() => { - let output: MediaItem[] = []; + const output: MediaItem[] = []; Object.entries(progressItems) .filter((entry) => shouldShowProgress(entry[1]).show) .sort((a, b) => b[1].updatedAt - a[1].updatedAt) @@ -36,35 +38,79 @@ export function WatchingPart({ }); }); - output = output.filter((v) => { - const isBookMarked = !!bookmarks[v.id]; - return !isBookMarked; - }); return output; - }, [progressItems, bookmarks]); + }, [progressItems]); useEffect(() => { onItemsChange(sortedProgressItems.length > 0); }, [sortedProgressItems, onItemsChange]); + const handleLongPress = () => { + // Find the button by ID and simulate a click + const editButton = document.getElementById("edit-button-watching"); + if (editButton) { + (editButton as HTMLButtonElement).click(); + } + }; + + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); // Prevent default touch action + pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION); + }; + + const handleTouchEnd = () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + pressTimerRef.current = null; + } + }; + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); // Prevent default mouse action + pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION); + }; + + const handleMouseUp = () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + pressTimerRef.current = null; + } + }; + if (sortedProgressItems.length === 0) return null; return ( -
+
) => + e.preventDefault() + } // Prevent right-click context menu + > - + {sortedProgressItems.map((v) => ( - removeItem(v.id)} - /> +
+ removeItem(v.id)} + /> +
))}
diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index 2d73286b..f6fb04d4 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -1,7 +1,10 @@ import { ReactNode } from "react"; +import { useParams } from "react-router-dom"; +import { Icon, Icons } from "@/components/Icon"; import { BrandPill } from "@/components/layout/BrandPill"; import { Player } from "@/components/player"; +import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { useIsMobile } from "@/hooks/useIsMobile"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; @@ -15,10 +18,17 @@ export interface PlayerPartProps { } export function PlayerPart(props: PlayerPartProps) { + const params = useParams<{ + media: string; + episode?: string; + season?: string; + }>(); + const media = params.media; const { showTargets, showTouchTargets } = useShouldShowControls(); const status = usePlayerStore((s) => s.status); const { isMobile } = useIsMobile(); const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); + const { playerMeta: meta } = usePlayerMeta(); return ( @@ -60,6 +70,30 @@ export function PlayerPart(props: PlayerPartProps) { / + + +
diff --git a/tailwind.config.ts b/tailwind.config.ts index 0732fcae..6e1c4a38 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -12,6 +12,9 @@ const config: Config = { /* breakpoints */ screens: { ssm: "400px", + '2xl': '1921px', // Custom breakpoint for screens at least 1920px wide + '3xl': '2650px', // Custom breakpoint for screens at least 2650px wide + '4xl': '3840px', // Custom breakpoint for screens at least 4096px wide }, /* fonts */ diff --git a/vite.config.mts b/vite.config.mts index 8e4074f8..6b0f5ee7 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -70,8 +70,8 @@ export default defineConfig(({ mode }) => { name: "sudo-flix", short_name: "sudo-flix", description: "Watch your favorite shows and movies for free with no ads ever! (っ'ヮ'c)", - theme_color: "#120f1d", - background_color: "#120f1d", + theme_color: "#000000", + background_color: "#000000", display: "standalone", start_url: "/", icons: [