Migration A

This commit is contained in:
Ivan Evans 2024-09-18 14:51:51 -06:00
parent c383dc1da4
commit 6575cb30a6
13 changed files with 306 additions and 49 deletions

View file

@ -18,9 +18,11 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#120f1d" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#120f1d" />
<meta name="msapplication-TileColor" content="#120f1d" /> <meta name="msapplication-TileColor" content="#120f1d" />
<meta name="theme-color" content="#120f1d" /> <meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-startup-image" <link rel="apple-touch-startup-image"
media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
href="/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png"> href="/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png">

View file

@ -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 base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html, html,
body { body {
font-family: "Lato", sans-serif !important;
@apply bg-background-main font-main text-type-text; @apply bg-background-main font-main text-type-text;
min-height: 100vh; min-height: 100vh;
min-height: 100dvh; min-height: 100dvh;
@ -79,6 +81,16 @@ html[data-no-scroll], html[data-no-scroll] body {
min-height: 100dvh; 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) /*generated with Input range slider CSS style generator (version 20211225)
https://toughengineer.github.io/demo/slider-styler*/ https://toughengineer.github.io/demo/slider-styler*/
:root { :root {

View file

@ -1,5 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useCallback } from "react"; import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
@ -7,31 +7,48 @@ import { Icon, Icons } from "@/components/Icon";
export interface EditButtonProps { export interface EditButtonProps {
editing: boolean; editing: boolean;
onEdit?: (editing: boolean) => void; onEdit?: (editing: boolean) => void;
id?: string;
} }
export function EditButton(props: EditButtonProps) { export function EditButton(props: EditButtonProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [parent] = useAutoAnimate<HTMLSpanElement>(); const [parent] = useAutoAnimate<HTMLSpanElement>();
const buttonRef = useRef<HTMLButtonElement>(null);
const onClick = useCallback(() => { const onClick = useCallback(() => {
props.onEdit?.(!props.editing); props.onEdit?.(!props.editing);
}, [props]); }, [props]);
return ( return (
<button <>
type="button" <button
onClick={onClick} ref={buttonRef}
className="flex h-12 items-center overflow-hidden rounded-full bg-background-secondary px-4 py-2 text-white transition-[background-color,transform] hover:bg-background-secondaryHover active:scale-105" type="button"
> onClick={onClick}
<span ref={parent}> className="flex h-12 items-center overflow-hidden rounded-full bg-background-secondary px-4 py-2 text-white transition-[background-color,transform] hover:bg-background-secondaryHover active:scale-105"
{props.editing ? ( id={props.id} // Assign id to the button
<span className="mx-2 sm:mx-4 whitespace-nowrap"> >
{t("home.mediaList.stopEditing")} <span ref={parent}>
</span> {props.editing ? (
) : ( <span className="mx-2 sm:mx-4 whitespace-nowrap">
<Icon icon={Icons.EDIT} /> {t("home.mediaList.stopEditing")}
)} </span>
</span> ) : (
</button> <Icon icon={Icons.EDIT} />
)}
</span>
</button>
{props.editing && (
<button
type="button"
onClick={onClick}
className="fixed bottom-9 right-7 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-background-secondary text-white border-2 border-green-500 transition-[background-color,transform,box-shadow] hover:bg-background-secondaryHover hover:scale-110 cursor-pointer"
id={props.id ? `${props.id}-check` : undefined} // Optionally use a different id for this button
>
<Icon icon={Icons.CHECKMARK} />
</button>
)}
</>
); );
} }

View file

@ -10,7 +10,9 @@ export function WideContainer(props: WideContainerProps) {
return ( return (
<div <div
className={`mx-auto max-w-full px-8 ${ className={`mx-auto max-w-full px-8 ${
props.ultraWide ? "w-[1300px] sm:px-16" : "w-[900px] sm:px-8" props.ultraWide
? "w-[1300px] 2xl:w-[2000px] 3xl:w-[2400px] 4xl:w-[2800px]"
: "w-[900px] 2xl:w-[1400px] 3xl:w-[1600px] 4xl:w-[1800px]"
} ${props.classNames || ""}`} } ${props.classNames || ""}`}
> >
{props.children} {props.children}

View file

@ -12,7 +12,7 @@ import { MediaItem } from "@/utils/mediaTypes";
import { MediaBookmarkButton } from "./MediaBookmark"; import { MediaBookmarkButton } from "./MediaBookmark";
import { IconPatch } from "../buttons/IconPatch"; import { IconPatch } from "../buttons/IconPatch";
import { Icons } from "../Icon"; import { Icon, Icons } from "../Icon";
export interface MediaCardProps { export interface MediaCardProps {
media: MediaItem; media: MediaItem;
@ -179,7 +179,29 @@ function MediaCardContent({
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white"> <h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
<span>{media.title}</span> <span>{media.title}</span>
</h1> </h1>
<DotList className="text-xs" content={dotListContent} /> <div className="media-info-container justify-content-center flex flex-wrap">
<DotList className="text-xs" content={dotListContent} />
<button
className="info-button"
type="button"
onClick={(e) => {
e.preventDefault();
const searchParam = encodeURIComponent(encodeURI(media.id));
const url =
media.type === "movie"
? `https://www.themoviedb.org/movie/${searchParam}`
: `https://www.themoviedb.org/tv/${searchParam}`;
window.open(url, "_blank");
}}
>
<Icon
className="text-xs font-semibold text-type-secondary"
icon={Icons.CIRCLE_QUESTION}
/>
</button>
</div>
</Flare.Child> </Flare.Child>
</Flare.Base> </Flare.Base>
); );

View file

@ -8,7 +8,7 @@ export const MediaGrid = forwardRef<HTMLDivElement, MediaGridProps>(
(props, ref) => { (props, ref) => {
return ( return (
<div <div
className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4" className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 2xl:grid-cols-6 3xl:grid-cols-8 4xl:grid-cols-10"
ref={ref} ref={ref}
> >
{props.children} {props.children}

View file

@ -23,6 +23,25 @@ import { SubPageLayout } from "./layouts/SubPageLayout";
import { Icon, Icons } from "../components/Icon"; import { Icon, Icons } from "../components/Icon";
import { PageTitle } from "./parts/util/PageTitle"; 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() { export function Discover() {
const { t } = useTranslation(); const { t } = useTranslation();
const [genres, setGenres] = useState<Genre[]>([]); const [genres, setGenres] = useState<Genre[]>([]);
@ -48,6 +67,45 @@ export function Discover() {
const [countdownTimeout, setCountdownTimeout] = const [countdownTimeout, setCountdownTimeout] =
useState<NodeJS.Timeout | null>(null); useState<NodeJS.Timeout | null>(null);
const [editorPicksData, setEditorPicksData] = useState<Media[]>([]);
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<any>(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(() => { useEffect(() => {
const fetchMoviesForCategory = async (category: Category) => { const fetchMoviesForCategory = async (category: Category) => {
try { try {
@ -315,11 +373,13 @@ export function Discover() {
const displayCategory = const displayCategory =
category === "Now Playing" category === "Now Playing"
? "In Cinemas" ? "In Cinemas"
: category.includes("Movie") : category === "Editor Picks" // Check for "Editor Picks" specifically
? `${category}s` ? category
: isTVShow : category.includes("Movie")
? `${category} Shows` ? `${category}s`
: `${category} Movies`; : isTVShow
? `${category} Shows`
: `${category} Movies`;
// https://tailwindcss.com/docs/border-style // https://tailwindcss.com/docs/border-style
return ( return (
@ -532,6 +592,16 @@ export function Discover() {
</p> </p>
</div> </div>
)} )}
{/* Editor Picks Section */}
<div className="mt-8">
{editorPicksData.length > 0 && (
<div className="mt-8">
{renderMovies(editorPicksData, "Editor Picks")}
</div>
)}
</div>
<div className="flex flex-col"> <div className="flex flex-col">
{categories.map((category) => ( {categories.map((category) => (
<div <div

View file

@ -74,8 +74,8 @@ export function HomePage() {
) : ( ) : (
<> <>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<BookmarksPart onItemsChange={setShowBookmarks} />
<WatchingPart onItemsChange={setShowWatching} /> <WatchingPart onItemsChange={setShowWatching} />
<BookmarksPart onItemsChange={setShowBookmarks} />
</div> </div>
{!(showBookmarks || showWatching) ? ( {!(showBookmarks || showWatching) ? (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">

View file

@ -1,5 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; 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 { useTranslation } from "react-i18next";
import { EditButton } from "@/components/buttons/EditButton"; import { EditButton } from "@/components/buttons/EditButton";
@ -11,6 +11,8 @@ import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress"; import { useProgressStore } from "@/stores/progress";
import { MediaItem } from "@/utils/mediaTypes"; import { MediaItem } from "@/utils/mediaTypes";
const LONG_PRESS_DURATION = 500; // 0.5 seconds
export function BookmarksPart({ export function BookmarksPart({
onItemsChange, onItemsChange,
}: { }: {
@ -23,6 +25,8 @@ export function BookmarksPart({
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [gridRef] = useAutoAnimate<HTMLDivElement>(); const [gridRef] = useAutoAnimate<HTMLDivElement>();
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
const items = useMemo(() => { const items = useMemo(() => {
let output: MediaItem[] = []; let output: MediaItem[] = [];
Object.entries(bookmarks).forEach((entry) => { Object.entries(bookmarks).forEach((entry) => {
@ -49,15 +53,60 @@ export function BookmarksPart({
onItemsChange(items.length > 0); onItemsChange(items.length > 0);
}, [items, onItemsChange]); }, [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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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; if (items.length === 0) return null;
return ( return (
<div> <div
className="relative"
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
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
>
<SectionHeading <SectionHeading
title={t("home.bookmarks.sectionTitle") || "Bookmarks"} title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
icon={Icons.BOOKMARK} icon={Icons.BOOKMARK}
> >
<EditButton editing={editing} onEdit={setEditing} /> <EditButton
editing={editing}
onEdit={setEditing}
id="edit-button-bookmark"
/>
</SectionHeading> </SectionHeading>
<MediaGrid ref={gridRef}> <MediaGrid ref={gridRef}>
{items.map((v) => ( {items.map((v) => (

View file

@ -1,5 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; 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 { useTranslation } from "react-i18next";
import { EditButton } from "@/components/buttons/EditButton"; import { EditButton } from "@/components/buttons/EditButton";
@ -7,25 +7,27 @@ import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading"; import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid"; import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress"; import { useProgressStore } from "@/stores/progress";
import { shouldShowProgress } from "@/stores/progress/utils"; import { shouldShowProgress } from "@/stores/progress/utils";
import { MediaItem } from "@/utils/mediaTypes"; import { MediaItem } from "@/utils/mediaTypes";
const LONG_PRESS_DURATION = 500; // 0.5 seconds
export function WatchingPart({ export function WatchingPart({
onItemsChange, onItemsChange,
}: { }: {
onItemsChange: (hasItems: boolean) => void; onItemsChange: (hasItems: boolean) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const bookmarks = useBookmarkStore((s) => s.bookmarks);
const progressItems = useProgressStore((s) => s.items); const progressItems = useProgressStore((s) => s.items);
const removeItem = useProgressStore((s) => s.removeItem); const removeItem = useProgressStore((s) => s.removeItem);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [gridRef] = useAutoAnimate<HTMLDivElement>(); const [gridRef] = useAutoAnimate<HTMLDivElement>();
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
const sortedProgressItems = useMemo(() => { const sortedProgressItems = useMemo(() => {
let output: MediaItem[] = []; const output: MediaItem[] = [];
Object.entries(progressItems) Object.entries(progressItems)
.filter((entry) => shouldShowProgress(entry[1]).show) .filter((entry) => shouldShowProgress(entry[1]).show)
.sort((a, b) => b[1].updatedAt - a[1].updatedAt) .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; return output;
}, [progressItems, bookmarks]); }, [progressItems]);
useEffect(() => { useEffect(() => {
onItemsChange(sortedProgressItems.length > 0); onItemsChange(sortedProgressItems.length > 0);
}, [sortedProgressItems, onItemsChange]); }, [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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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; if (sortedProgressItems.length === 0) return null;
return ( return (
<div> <div
className="relative"
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
e.preventDefault()
} // Prevent right-click context menu
>
<SectionHeading <SectionHeading
title={t("home.continueWatching.sectionTitle")} title={t("home.continueWatching.sectionTitle")}
icon={Icons.CLOCK} icon={Icons.CLOCK}
> >
<EditButton editing={editing} onEdit={setEditing} /> <EditButton
editing={editing}
onEdit={setEditing}
id="edit-button-watching"
/>
</SectionHeading> </SectionHeading>
<MediaGrid ref={gridRef}> <MediaGrid ref={gridRef}>
{sortedProgressItems.map((v) => ( {sortedProgressItems.map((v) => (
<WatchedMediaCard <div
key={v.id} onTouchStart={handleTouchStart} // Handle touch start
media={v} onTouchEnd={handleTouchEnd} // Handle touch end
closable={editing} onMouseDown={handleMouseDown} // Handle mouse down
onClose={() => removeItem(v.id)} onMouseUp={handleMouseUp} // Handle mouse up
/> >
<WatchedMediaCard
key={v.id}
media={v}
closable={editing}
onClose={() => removeItem(v.id)}
/>
</div>
))} ))}
</MediaGrid> </MediaGrid>
</div> </div>

View file

@ -1,7 +1,10 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { useParams } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon";
import { BrandPill } from "@/components/layout/BrandPill"; import { BrandPill } from "@/components/layout/BrandPill";
import { Player } from "@/components/player"; import { Player } from "@/components/player";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
import { useIsMobile } from "@/hooks/useIsMobile"; import { useIsMobile } from "@/hooks/useIsMobile";
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
@ -15,10 +18,17 @@ export interface PlayerPartProps {
} }
export function PlayerPart(props: PlayerPartProps) { export function PlayerPart(props: PlayerPartProps) {
const params = useParams<{
media: string;
episode?: string;
season?: string;
}>();
const media = params.media;
const { showTargets, showTouchTargets } = useShouldShowControls(); const { showTargets, showTouchTargets } = useShouldShowControls();
const status = usePlayerStore((s) => s.status); const status = usePlayerStore((s) => s.status);
const { isMobile } = useIsMobile(); const { isMobile } = useIsMobile();
const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading);
const { playerMeta: meta } = usePlayerMeta();
return ( return (
<Player.Container onLoad={props.onLoad} showingControls={showTargets}> <Player.Container onLoad={props.onLoad} showingControls={showTargets}>
@ -60,6 +70,30 @@ export function PlayerPart(props: PlayerPartProps) {
<Player.BackLink url={props.backUrl} /> <Player.BackLink url={props.backUrl} />
<span className="text mx-3 text-type-secondary">/</span> <span className="text mx-3 text-type-secondary">/</span>
<Player.Title /> <Player.Title />
<button
type="button"
onClick={(e) => {
e.preventDefault();
if (!media) return;
const id = media
.replace("tmdb-tv-", "")
.replace("tmdb-movie-", "");
let url;
if (meta?.type === "movie") {
url = `https://www.themoviedb.org/movie/${id}`;
} else {
url = `https://www.themoviedb.org/tv/${id}`;
}
window.open(url, "_blank");
}}
>
<Icon
className="text-xs font-semibold text-type-secondary"
icon={Icons.CIRCLE_QUESTION}
/>
</button>
<Player.BookmarkButton /> <Player.BookmarkButton />
</div> </div>
<div className="text-center hidden xl:flex justify-center items-center"> <div className="text-center hidden xl:flex justify-center items-center">

View file

@ -12,6 +12,9 @@ const config: Config = {
/* breakpoints */ /* breakpoints */
screens: { screens: {
ssm: "400px", 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 */ /* fonts */

View file

@ -70,8 +70,8 @@ export default defineConfig(({ mode }) => {
name: "sudo-flix", name: "sudo-flix",
short_name: "sudo-flix", short_name: "sudo-flix",
description: "Watch your favorite shows and movies for free with no ads ever! (っ'ヮ'c)", description: "Watch your favorite shows and movies for free with no ads ever! (っ'ヮ'c)",
theme_color: "#120f1d", theme_color: "#000000",
background_color: "#120f1d", background_color: "#000000",
display: "standalone", display: "standalone",
start_url: "/", start_url: "/",
icons: [ icons: [