diff --git a/src/__tests__/subtitles/subtitles.test.ts b/src/__tests__/subtitles/subtitles.test.ts index 50270d5c..69934f8f 100644 --- a/src/__tests__/subtitles/subtitles.test.ts +++ b/src/__tests__/subtitles/subtitles.test.ts @@ -1,10 +1,12 @@ import { describe, it } from "vitest"; + import { - isSupportedSubtitle, getMWCaptionTypeFromUrl, + isSupportedSubtitle, parseSubtitles, } from "@/backend/helpers/captions"; import { MWCaptionType } from "@/backend/helpers/streams"; + import { ass, multilineSubtitlesTestVtt, diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index 61568af3..f0f559bd 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -1,13 +1,13 @@ import { compareTitle } from "@/utils/titleMatch"; -import { proxiedFetch } from "../helpers/fetch"; -import { registerProvider } from "../helpers/register"; -import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types"; import { getMWCaptionTypeFromUrl, isSupportedSubtitle, } from "../helpers/captions"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams"; +import { MWMediaType } from "../metadata/types"; const flixHqBase = "https://api.consumet.org/meta/tmdb"; diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 0730708b..435c36bb 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -1,6 +1,10 @@ import CryptoJS from "crypto-js"; import { customAlphabet } from "nanoid"; +import { + getMWCaptionTypeFromUrl, + isSupportedSubtitle, +} from "@/backend/helpers/captions"; import { proxiedFetch } from "@/backend/helpers/fetch"; import { registerProvider } from "@/backend/helpers/register"; import { @@ -11,10 +15,6 @@ import { } from "@/backend/helpers/streams"; import { MWMediaType } from "@/backend/metadata/types"; import { compareTitle } from "@/utils/titleMatch"; -import { - getMWCaptionTypeFromUrl, - isSupportedSubtitle, -} from "@/backend/helpers/captions"; const nanoid = customAlphabet("0123456789abcdef", 32); diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index e2fac636..aff10ea4 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -37,7 +37,7 @@ export function Dropdown(props: DropdownProps) { leaveFrom="opacity-100" leaveTo="opacity-0" > - <Listbox.Options className="absolute top-10 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10 sm:text-sm"> + <Listbox.Options className="absolute left-0 right-0 top-10 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10 sm:text-sm"> {props.options.map((opt) => ( <Listbox.Option className={({ active }) => diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 2513e409..4940cbc7 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -40,7 +40,7 @@ export function SearchBarInput(props: SearchBarProps) { return ( <div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center"> - <div className="pointer-events-none absolute left-5 top-0 bottom-0 flex max-h-14 items-center"> + <div className="pointer-events-none absolute bottom-0 left-5 top-0 flex max-h-14 items-center"> <Icon icon={Icons.SEARCH} /> </div> @@ -52,7 +52,7 @@ export function SearchBarInput(props: SearchBarProps) { placeholder={props.placeholder} /> - <div className="px-4 py-4 pt-0 sm:py-2 sm:px-2"> + <div className="px-4 py-4 pt-0 sm:px-2 sm:py-2"> <DropdownButton icon={Icons.SEARCH} open={dropdownOpen} diff --git a/src/components/layout/Backdrop.tsx b/src/components/layout/Backdrop.tsx index 7e022347..0286880e 100644 --- a/src/components/layout/Backdrop.tsx +++ b/src/components/layout/Backdrop.tsx @@ -100,7 +100,7 @@ export function BackdropContainer( return ( <div ref={root}> {createPortal( - <div className="pointer-events-none fixed top-0 left-0 z-[999]"> + <div className="pointer-events-none fixed left-0 top-0 z-[999]"> <Backdrop active={props.active} {...props} /> <div ref={copy} className="pointer-events-auto absolute"> {props.children} diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index df6d464b..a9a3e0c1 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -24,7 +24,7 @@ export function Navigation(props: NavigationProps) { top: `${bannerHeight}px`, }} > - <div className="fixed left-0 right-0 flex items-center justify-between py-5 px-7"> + <div className="fixed left-0 right-0 flex items-center justify-between px-7 py-5"> <div className={`${ props.bg ? "opacity-100" : "opacity-0" diff --git a/src/components/media/EpisodeButton.tsx b/src/components/media/EpisodeButton.tsx index f3e4375c..76e38c85 100644 --- a/src/components/media/EpisodeButton.tsx +++ b/src/components/media/EpisodeButton.tsx @@ -9,12 +9,12 @@ export function Episode(props: EpisodeProps) { return ( <div onClick={props.onClick} - className={`transition-[background-color, transform, box-shadow] relative mr-3 mb-3 inline-flex h-10 w-10 cursor-pointer select-none items-center justify-center overflow-hidden rounded bg-denim-500 font-bold text-white hover:bg-denim-400 active:scale-110 ${ + className={`transition-[background-color, transform, box-shadow] relative mb-3 mr-3 inline-flex h-10 w-10 cursor-pointer select-none items-center justify-center overflow-hidden rounded bg-denim-500 font-bold text-white hover:bg-denim-400 active:scale-110 ${ props.active ? "shadow-[inset_0_0_0_2px] shadow-bink-500" : "" }`} > <div - className="absolute bottom-0 top-0 left-0 bg-bink-500 bg-opacity-50" + className="absolute bottom-0 left-0 top-0 bg-bink-500 bg-opacity-50" style={{ width: `${props.progress || 0}%`, }} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 3769245e..22865717 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -61,7 +61,7 @@ function MediaCardContent({ {series ? ( <div className={[ - "absolute right-2 top-2 rounded-md bg-denim-200 py-1 px-2 transition-colors", + "absolute right-2 top-2 rounded-md bg-denim-200 px-2 py-1 transition-colors", closable ? "" : "group-hover:bg-denim-500", ].join(" ")} > diff --git a/src/components/popout/FloatingCard.tsx b/src/components/popout/FloatingCard.tsx index 5a837d01..b4fd250c 100644 --- a/src/components/popout/FloatingCard.tsx +++ b/src/components/popout/FloatingCard.tsx @@ -167,7 +167,7 @@ export const FloatingCardView = { <div>{props.action ?? null}</div> </div> - <h2 className="mt-8 mb-2 text-3xl font-bold text-white"> + <h2 className="mb-2 mt-8 text-3xl font-bold text-white"> {props.title} </h2> <p>{props.description}</p> diff --git a/src/utils/thumbnailCreator.ts b/src/utils/thumbnailCreator.ts new file mode 100644 index 00000000..159f4349 --- /dev/null +++ b/src/utils/thumbnailCreator.ts @@ -0,0 +1,41 @@ +export default async function extractThumbnails( + videoUrl: string, + numThumbnails: number +): Promise<string[]> { + const video = document.createElement("video"); + video.src = videoUrl; + video.crossOrigin = "anonymous"; + + // Wait for the video metadata to load + const metadata = await new Promise((resolve, reject) => { + video.addEventListener("loadedmetadata", resolve); + video.addEventListener("error", reject); + }); + console.log(metadata); + + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext("2d"); + const thumbnails = []; + if (!ctx) return [""]; + + for (let i = 0; i < numThumbnails; i += 1) { + const time = ((i + 1) / (numThumbnails + 1)) * video.duration; + + // Seek to the specified time + video.currentTime = time; + await new Promise((resolve) => { + video.addEventListener("seeked", resolve); + }); + + // Draw the video frame on the canvas + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Convert the canvas to a data URL and add it to the list of thumbnails + const thumbnailUrl = canvas.toDataURL("image/jpeg", 0.8); + thumbnails.push(thumbnailUrl); + } + + return thumbnails; +} diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 6dc8983a..eca0477e 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -120,7 +120,7 @@ export function VideoPlayer(props: Props) { <Transition animation="slide-down" show={show} - className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2" + className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col px-8 py-6 pb-2" > <HeaderAction showControls={isMobile} diff --git a/src/video/components/actions/VolumeAdjustedAction.tsx b/src/video/components/actions/VolumeAdjustedAction.tsx index a5c547c5..295f3883 100644 --- a/src/video/components/actions/VolumeAdjustedAction.tsx +++ b/src/video/components/actions/VolumeAdjustedAction.tsx @@ -14,7 +14,7 @@ export function VolumeAdjustedAction() { videoInterface.volumeChangedWithKeybind ? "mt-10 scale-100 opacity-100" : "mt-5 scale-75 opacity-0", - "absolute left-1/2 z-[100] flex -translate-x-1/2 items-center space-x-4 rounded-full bg-bink-300 bg-opacity-50 py-2 px-5 transition-all duration-100", + "absolute left-1/2 z-[100] flex -translate-x-1/2 items-center space-x-4 rounded-full bg-bink-300 bg-opacity-50 px-5 py-2 transition-all duration-100", ].join(" ")} > <Icon diff --git a/src/video/components/actions/list-entries/QualityDisplayAction.tsx b/src/video/components/actions/list-entries/QualityDisplayAction.tsx index c2a58c50..aef30a06 100644 --- a/src/video/components/actions/list-entries/QualityDisplayAction.tsx +++ b/src/video/components/actions/list-entries/QualityDisplayAction.tsx @@ -8,7 +8,7 @@ export function QualityDisplayAction() { if (!source.source) return null; return ( - <div className="rounded-md bg-denim-300 py-1 px-2 transition-colors"> + <div className="rounded-md bg-denim-300 px-2 py-1 transition-colors"> <p className="text-center text-xs font-bold text-slate-300 transition-colors"> {source.source.quality} </p> diff --git a/src/video/components/parts/VideoErrorBoundary.tsx b/src/video/components/parts/VideoErrorBoundary.tsx index 8228f057..5786aa7a 100644 --- a/src/video/components/parts/VideoErrorBoundary.tsx +++ b/src/video/components/parts/VideoErrorBoundary.tsx @@ -64,7 +64,7 @@ export class VideoErrorBoundary extends Component< return ( <div className="absolute inset-0 bg-denim-100"> - <div className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2"> + <div className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col px-8 py-6 pb-2"> <VideoPlayerHeader media={this.props.media} onClick={this.props.onGoBack} diff --git a/src/video/components/parts/VideoPlayerError.tsx b/src/video/components/parts/VideoPlayerError.tsx index d6fd0306..09177ddb 100644 --- a/src/video/components/parts/VideoPlayerError.tsx +++ b/src/video/components/parts/VideoPlayerError.tsx @@ -32,7 +32,7 @@ export function VideoPlayerError(props: VideoPlayerErrorProps) { {err?.name}: {err?.description} </p> </div> - <div className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2"> + <div className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col px-8 py-6 pb-2"> <VideoPlayerHeader media={meta?.meta.meta} onClick={props.onGoBack} /> </div> </div> diff --git a/src/views/developer/VideoTesterView.tsx b/src/views/developer/VideoTesterView.tsx index 4c3cb7e5..b192cd40 100644 --- a/src/views/developer/VideoTesterView.tsx +++ b/src/views/developer/VideoTesterView.tsx @@ -51,7 +51,7 @@ export default function VideoTesterView() { if (video) { return ( - <div className="fixed top-0 left-0 h-[100dvh] w-screen"> + <div className="fixed left-0 top-0 h-[100dvh] w-screen"> <Helmet> <html data-full="true" /> </Helmet> diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx index e79d3521..b9c88012 100644 --- a/src/views/media/MediaErrorView.tsx +++ b/src/views/media/MediaErrorView.tsx @@ -14,7 +14,7 @@ export function MediaFetchErrorView() { <Helmet> <title>{t("media.errors.failedMeta")}</title> </Helmet> - <div className="fixed inset-x-0 top-0 py-6 px-8"> + <div className="fixed inset-x-0 top-0 px-8 py-6"> <VideoPlayerHeader onClick={goBack} /> </div> <ErrorMessage> diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 132161dd..c55211c7 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -34,7 +34,7 @@ function MediaViewLoading(props: { onGoBack(): void }) { <Helmet> <title>{t("videoPlayer.loading")}</title> </Helmet> - <div className="absolute inset-x-0 top-0 py-6 px-8"> + <div className="absolute inset-x-0 top-0 px-8 py-6"> <VideoPlayerHeader onClick={props.onGoBack} /> </div> <div className="flex flex-col items-center"> @@ -68,7 +68,7 @@ function MediaViewScraping(props: MediaViewScrapingProps) { <Helmet> <title>{props.meta.meta.title}</title> </Helmet> - <div className="absolute inset-x-0 top-0 py-6 px-8"> + <div className="absolute inset-x-0 top-0 px-8 py-6"> <VideoPlayerHeader onClick={props.onGoBack} media={props.meta.meta} /> </div> <div className="flex flex-col items-center transition-opacity duration-200"> @@ -134,7 +134,7 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { } return ( - <div className="fixed top-0 left-0 h-[100dvh] w-screen"> + <div className="fixed left-0 top-0 h-[100dvh] w-screen"> <Helmet> <html data-full="true" /> </Helmet> diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index b7dfccf1..21ba4c63 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -23,7 +23,7 @@ export function NotFoundWrapper(props: { <title>{t("notFound.genericTitle")}</title> </Helmet> {props.video ? ( - <div className="absolute inset-x-0 top-0 py-6 px-8"> + <div className="absolute inset-x-0 top-0 px-8 py-6"> <VideoPlayerHeader onClick={goBack} /> </div> ) : ( @@ -46,7 +46,7 @@ export function NotFoundMedia() { className="mb-6 text-xl text-bink-600" /> <Title>{t("notFound.media.title")}</Title> - <p className="mt-5 mb-12 max-w-sm">{t("notFound.media.description")}</p> + <p className="mb-12 mt-5 max-w-sm">{t("notFound.media.description")}</p> <ArrowLink to="/" linkText={t("notFound.backArrow")} /> </div> ); @@ -62,7 +62,7 @@ export function NotFoundProvider() { className="mb-6 text-xl text-bink-600" /> <Title>{t("notFound.provider.title")}</Title> - <p className="mt-5 mb-12 max-w-sm"> + <p className="mb-12 mt-5 max-w-sm"> {t("notFound.provider.description")} </p> <ArrowLink to="/" linkText={t("notFound.backArrow")} /> @@ -80,7 +80,7 @@ export function NotFoundPage() { className="mb-6 text-xl text-bink-600" /> <Title>{t("notFound.page.title")}</Title> - <p className="mt-5 mb-12 max-w-sm">{t("notFound.page.description")}</p> + <p className="mb-12 mt-5 max-w-sm">{t("notFound.page.description")}</p> <ArrowLink to="/" linkText={t("notFound.backArrow")} /> </NotFoundWrapper> ); diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index bfb64695..a7a9a396 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -169,7 +169,7 @@ function NewDomainModal() { }} /> <div className="relative flex items-center justify-center"> - <div className="rounded-full bg-bink-200 py-4 px-12 text-center text-sm font-bold text-white md:text-xl"> + <div className="rounded-full bg-bink-200 px-12 py-4 text-center text-sm font-bold text-white md:text-xl"> {t("v3.newDomain")} </div> </div> @@ -186,7 +186,7 @@ function NewDomainModal() { </p> <p>{t("v3.tireless")}</p> </div> - <div className="mt-16 mb-6 flex items-center justify-center"> + <div className="mb-6 mt-16 flex items-center justify-center"> <Button icon={Icons.PLAY} onClick={() => closeModal()}> {t("v3.leaveAnnouncement")} </Button> diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 4c59d677..45971860 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -8,7 +8,7 @@ export function SearchLoadingView() { const [query] = useSearchQuery(); return ( <Loading - className="mt-40 mb-24 " + className="mb-24 mt-40 " text={ t(`search.loading_${query.type}`) || t("search.loading") || diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 25d347c9..331d4f2d 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -18,7 +18,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) { const icon: Icons = props.failed ? Icons.WARNING : Icons.EYE_SLASH; return ( - <div className="mt-40 mb-24 flex flex-col items-center justify-center space-y-3 text-center"> + <div className="mb-24 mt-40 flex flex-col items-center justify-center space-y-3 text-center"> <IconPatch icon={icon} className={`text-xl ${props.failed ? "text-red-400" : "text-bink-600"}`} diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index fce4ce08..3fee80b8 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -33,7 +33,7 @@ export function SearchView() { <Navigation bg={showBg} /> <ThinContainer> <div className="mt-44 space-y-16 text-center"> - <div className="absolute left-0 bottom-0 right-0 flex h-0 justify-center"> + <div className="absolute bottom-0 left-0 right-0 flex h-0 justify-center"> <div className="absolute bottom-4 h-[100vh] w-[3000px] rounded-[100%] bg-denim-300 md:w-[200vw]" /> </div> <div className="relative z-10 mb-16">