Use import alias prefix

This commit is contained in:
Rihan 2022-12-13 22:50:13 +00:00 committed by GitHub
parent d158259f30
commit 0ee269c877
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 478 additions and 330 deletions

View File

@ -1,8 +1,8 @@
import { MWMediaType } from "providers";
import { Redirect, Route, Switch } from "react-router-dom"; import { Redirect, Route, Switch } from "react-router-dom";
import { BookmarkContextProvider } from "state/bookmark"; import { MWMediaType } from "@/providers";
import { WatchedContextProvider } from "state/watched"; import { BookmarkContextProvider } from "@/state/bookmark";
import { NotFoundPage } from "views/notfound/NotFoundView"; import { WatchedContextProvider } from "@/state/watched";
import { NotFoundPage } from "@/views/notfound/NotFoundView";
import "./index.css"; import "./index.css";
import { MediaView } from "./views/MediaView"; import { MediaView } from "./views/MediaView";
import { SearchView } from "./views/SearchView"; import { SearchView } from "./views/SearchView";

View File

@ -1,7 +1,7 @@
import { Icon, Icons } from "components/Icon";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
import { Icon, Icons } from "@/components/Icon";
export interface OptionItem { export interface OptionItem {
id: string; id: string;
@ -20,7 +20,7 @@ export const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}> <Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
{({ open }) => ( {({ open }) => (
<> <>
<Listbox.Button className="bg-denim-500 focus-visible:ring-bink-500 focus-visible:ring-offset-bink-300 relative w-full cursor-default rounded-lg py-2 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm"> <Listbox.Button className="relative w-full cursor-default rounded-lg bg-denim-500 py-2 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-bink-500 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-bink-300 sm:text-sm">
<span className="block truncate">{props.selectedItem.name}</span> <span className="block truncate">{props.selectedItem.name}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<Icon <Icon
@ -37,7 +37,7 @@ export const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="bg-denim-500 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 absolute bottom-11 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:bottom-10 sm:text-sm"> <Listbox.Options className="absolute bottom-11 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:bottom-10 sm:text-sm">
{props.options.map((opt) => ( {props.options.map((opt) => (
<Listbox.Option <Listbox.Option
className={({ active }) => className={({ active }) =>

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { MWMediaType, MWQuery } from "providers"; import { MWMediaType, MWQuery } from "@/providers";
import { DropdownButton } from "./buttons/DropdownButton"; import { DropdownButton } from "./buttons/DropdownButton";
import { Icons } from "./Icon"; import { Icons } from "./Icon";
import { TextInputControl } from "./text-inputs/TextInputControl"; import { TextInputControl } from "./text-inputs/TextInputControl";

View File

@ -1,12 +1,12 @@
import { Icon, Icons } from "components/Icon";
import React, { import React, {
MouseEventHandler, MouseEventHandler,
SyntheticEvent, SyntheticEvent,
useEffect, useEffect,
useState, useState,
} from "react"; } from "react";
import { Icon, Icons } from "@/components/Icon";
import { Backdrop, useBackdrop } from "components/layout/Backdrop"; import { Backdrop, useBackdrop } from "@/components/layout/Backdrop";
import { ButtonControlProps, ButtonControl } from "./ButtonControl"; import { ButtonControlProps, ButtonControl } from "./ButtonControl";
export interface OptionItem { export interface OptionItem {
@ -33,7 +33,7 @@ export interface OptionProps {
function Option({ option, onClick, tabIndex }: OptionProps) { function Option({ option, onClick, tabIndex }: OptionProps) {
return ( return (
<div <div
className="text-denim-700 flex h-10 cursor-pointer items-center space-x-2 px-4 py-2 text-left transition-colors hover:text-white" className="flex h-10 cursor-pointer items-center space-x-2 px-4 py-2 text-left text-denim-700 transition-colors hover:text-white"
onClick={onClick} onClick={onClick}
tabIndex={tabIndex} tabIndex={tabIndex}
> >
@ -95,7 +95,7 @@ export const DropdownButton = React.forwardRef<
> >
<ButtonControl <ButtonControl
{...props} {...props}
className="sm:justify-left bg-bink-200 hover:bg-bink-300 relative z-20 flex h-10 w-full items-center justify-center space-x-2 rounded-[20px] px-4 py-2 text-white" className="sm:justify-left relative z-20 flex h-10 w-full items-center justify-center space-x-2 rounded-[20px] bg-bink-200 px-4 py-2 text-white hover:bg-bink-300"
> >
<Icon icon={selectedItem.icon} /> <Icon icon={selectedItem.icon} />
<span className="flex-1">{selectedItem.name}</span> <span className="flex-1">{selectedItem.name}</span>
@ -105,7 +105,7 @@ export const DropdownButton = React.forwardRef<
/> />
</ButtonControl> </ButtonControl>
<div <div
className={`bg-denim-300 absolute top-0 z-10 w-full rounded-[20px] pt-[40px] transition-all duration-200 ${ className={`absolute top-0 z-10 w-full rounded-[20px] bg-denim-300 pt-[40px] transition-all duration-200 ${
props.open props.open
? "block max-h-60 opacity-100" ? "block max-h-60 opacity-100"
: "invisible max-h-0 opacity-0" : "invisible max-h-0 opacity-0"

View File

@ -1,4 +1,4 @@
import { Icon, Icons } from "components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { ButtonControlProps, ButtonControl } from "./ButtonControl"; import { ButtonControlProps, ButtonControl } from "./ButtonControl";
export interface IconButtonProps extends ButtonControlProps { export interface IconButtonProps extends ButtonControlProps {
@ -9,7 +9,7 @@ export function IconButton(props: IconButtonProps) {
return ( return (
<ButtonControl <ButtonControl
{...props} {...props}
className="flex items-center px-4 py-2 space-x-2 bg-bink-200 hover:bg-bink-300 text-white rounded-full" className="flex items-center space-x-2 rounded-full bg-bink-200 px-4 py-2 text-white hover:bg-bink-300"
> >
<Icon icon={props.icon} /> <Icon icon={props.icon} />
<span>{props.children}</span> <span>{props.children}</span>

View File

@ -1,4 +1,4 @@
import { Icon, Icons } from "components/Icon"; import { Icon, Icons } from "@/components/Icon";
export interface IconPatchProps { export interface IconPatchProps {
active?: boolean; active?: boolean;
@ -12,11 +12,11 @@ export function IconPatch(props: IconPatchProps) {
return ( return (
<div className={props.className || undefined} onClick={props.onClick}> <div className={props.className || undefined} onClick={props.onClick}>
<div <div
className={`bg-denim-300 flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent transition-[color,transform,border-color] duration-75 ${ className={`flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent bg-denim-300 transition-[color,transform,border-color] duration-75 ${
props.clickable props.clickable
? "hover:bg-denim-400 cursor-pointer hover:scale-110 hover:text-white active:scale-125" ? "cursor-pointer hover:scale-110 hover:bg-denim-400 hover:text-white active:scale-125"
: "" : ""
} ${props.active ? "text-bink-600 border-bink-600 bg-bink-100" : ""}`} } ${props.active ? "border-bink-600 bg-bink-100 text-bink-600" : ""}`}
> >
<Icon icon={props.icon} /> <Icon icon={props.icon} />
</div> </div>

View File

@ -1,5 +1,5 @@
import { useFade } from "hooks/useFade";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useFade } from "@/hooks/useFade";
interface BackdropProps { interface BackdropProps {
onClick?: (e: MouseEvent) => void; onClick?: (e: MouseEvent) => void;

View File

@ -1,11 +1,11 @@
import { Icon, Icons } from "components/Icon"; import { Icon, Icons } from "@/components/Icon";
export function BrandPill(props: { clickable?: boolean }) { export function BrandPill(props: { clickable?: boolean }) {
return ( return (
<div <div
className={`bg-bink-100 text-bink-600 flex items-center space-x-2 rounded-full bg-opacity-50 px-4 py-2 ${ className={`flex items-center space-x-2 rounded-full bg-bink-100 bg-opacity-50 px-4 py-2 text-bink-600 ${
props.clickable props.clickable
? "hover:bg-bink-200 hover:text-bink-700 transition-[transform,background-color] hover:scale-105 active:scale-95" ? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95"
: "" : ""
}`} }`}
> >

View File

@ -1,9 +1,9 @@
import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon";
import { Link } from "components/text/Link";
import { Title } from "components/text/Title";
import { DISCORD_LINK, GITHUB_LINK } from "mw_constants";
import { Component } from "react"; import { Component } from "react";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Link } from "@/components/text/Link";
import { Title } from "@/components/text/Title";
import { DISCORD_LINK, GITHUB_LINK } from "@/mw_constants";
interface ErrorBoundaryState { interface ErrorBoundaryState {
hasError: boolean; hasError: boolean;

View File

@ -1,8 +1,8 @@
import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon";
import { DISCORD_LINK, GITHUB_LINK } from "mw_constants";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { DISCORD_LINK, GITHUB_LINK } from "@/mw_constants";
import { BrandPill } from "./BrandPill"; import { BrandPill } from "./BrandPill";
export interface NavigationProps { export interface NavigationProps {
@ -11,8 +11,8 @@ export interface NavigationProps {
export function Navigation(props: NavigationProps) { export function Navigation(props: NavigationProps) {
return ( return (
<div className="absolute left-0 right-0 top-0 flex items-center justify-between py-5 px-7 min-h-[88px]"> <div className="absolute left-0 right-0 top-0 flex min-h-[88px] items-center justify-between py-5 px-7">
<div className="flex items-center justify-center w-full sm:w-fit"> <div className="flex w-full items-center justify-center sm:w-fit">
<div className="mr-auto sm:mr-6"> <div className="mr-auto sm:mr-6">
<Link to="/"> <Link to="/">
<BrandPill clickable /> <BrandPill clickable />
@ -20,7 +20,11 @@ export function Navigation(props: NavigationProps) {
</div> </div>
{props.children} {props.children}
</div> </div>
<div className={`${props.children ? "hidden sm:flex" : "flex"} flex-row gap-4`}> <div
className={`${
props.children ? "hidden sm:flex" : "flex"
} flex-row gap-4`}
>
<a <a
href={DISCORD_LINK} href={DISCORD_LINK}
target="_blank" target="_blank"

View File

@ -1,19 +1,19 @@
import { IconPatch } from "components/buttons/IconPatch"; import { useEffect, useState } from "react";
import { Dropdown, OptionItem } from "components/Dropdown"; import { useHistory } from "react-router-dom";
import { Icons } from "components/Icon"; import { IconPatch } from "@/components/buttons/IconPatch";
import { WatchedEpisode } from "components/media/WatchedEpisodeButton"; import { Dropdown, OptionItem } from "@/components/Dropdown";
import { useLoading } from "hooks/useLoading"; import { Icons } from "@/components/Icon";
import { serializePortableMedia } from "hooks/usePortableMedia"; import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton";
import { useLoading } from "@/hooks/useLoading";
import { serializePortableMedia } from "@/hooks/usePortableMedia";
import { import {
convertMediaToPortable, convertMediaToPortable,
MWMedia, MWMedia,
MWMediaSeasons, MWMediaSeasons,
MWMediaSeason, MWMediaSeason,
MWPortableMedia, MWPortableMedia,
} from "providers"; } from "@/providers";
import { getSeasonDataFromMedia } from "providers/methods/seasons"; import { getSeasonDataFromMedia } from "@/providers/methods/seasons";
import { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
export interface SeasonsProps { export interface SeasonsProps {
media: MWMedia; media: MWMedia;
@ -23,13 +23,13 @@ export function LoadingSeasons(props: { error?: boolean }) {
return ( return (
<div> <div>
<div> <div>
<div className="bg-denim-400 mb-3 mt-5 h-10 w-56 rounded opacity-50" /> <div className="mb-3 mt-5 h-10 w-56 rounded bg-denim-400 opacity-50" />
</div> </div>
{!props.error ? ( {!props.error ? (
<> <>
<div className="bg-denim-400 mr-3 mb-3 inline-block h-10 w-10 rounded opacity-50" /> <div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
<div className="bg-denim-400 mr-3 mb-3 inline-block h-10 w-10 rounded opacity-50" /> <div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
<div className="bg-denim-400 mr-3 mb-3 inline-block h-10 w-10 rounded opacity-50" /> <div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
</> </>
) : ( ) : (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">

View File

@ -1,6 +1,6 @@
import { Icon, Icons } from "components/Icon";
import { ArrowLink } from "components/text/ArrowLink";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Icon, Icons } from "@/components/Icon";
import { ArrowLink } from "@/components/text/ArrowLink";
interface SectionHeadingProps { interface SectionHeadingProps {
icon?: Icons; icon?: Icons;
@ -15,7 +15,7 @@ export function SectionHeading(props: SectionHeadingProps) {
return ( return (
<div className={`mt-12 ${props.className}`}> <div className={`mt-12 ${props.className}`}>
<div className="mb-4 flex items-end"> <div className="mb-4 flex items-end">
<p className="text-denim-700 flex flex-1 items-center font-bold uppercase"> <p className="flex flex-1 items-center font-bold uppercase text-denim-700">
{props.icon ? ( {props.icon ? (
<span className="mr-2 text-xl"> <span className="mr-2 text-xl">
<Icon icon={props.icon} /> <Icon icon={props.icon} />

View File

@ -1,13 +1,13 @@
import { Link } from "react-router-dom";
import { import {
convertMediaToPortable, convertMediaToPortable,
getProviderFromId, getProviderFromId,
MWMediaMeta, MWMediaMeta,
MWMediaType, MWMediaType,
} from "providers"; } from "@/providers";
import { Link } from "react-router-dom"; import { Icon, Icons } from "@/components/Icon";
import { Icon, Icons } from "components/Icon"; import { serializePortableMedia } from "@/hooks/usePortableMedia";
import { serializePortableMedia } from "hooks/usePortableMedia"; import { DotList } from "@/components/text/DotList";
import { DotList } from "components/text/DotList";
export interface MediaCardProps { export interface MediaCardProps {
media: MWMediaMeta; media: MWMediaMeta;
@ -30,7 +30,7 @@ function MediaCardContent({
return ( return (
<article <article
className={`bg-denim-300 group relative mb-4 flex overflow-hidden rounded py-4 px-5 ${ className={`group relative mb-4 flex overflow-hidden rounded bg-denim-300 py-4 px-5 ${
linkable ? "hover:bg-denim-400" : "" linkable ? "hover:bg-denim-400" : ""
}`} }`}
> >
@ -38,12 +38,12 @@ function MediaCardContent({
{watchedPercentage > 0 ? ( {watchedPercentage > 0 ? (
<div className="absolute top-0 left-0 right-0 bottom-0"> <div className="absolute top-0 left-0 right-0 bottom-0">
<div <div
className="bg-bink-300 relative h-full bg-opacity-30" className="relative h-full bg-bink-300 bg-opacity-30"
style={{ style={{
width: `${watchedPercentage}%`, width: `${watchedPercentage}%`,
}} }}
> >
<div className="from-bink-400 absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l to-transparent opacity-40" /> <div className="absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l from-bink-400 to-transparent opacity-40" />
</div> </div>
</div> </div>
) : null} ) : null}
@ -54,7 +54,7 @@ function MediaCardContent({
<h1 className="mb-1 font-bold text-white"> <h1 className="mb-1 font-bold text-white">
{media.title} {media.title}
{series && media.seasonId && media.episodeId ? ( {series && media.seasonId && media.episodeId ? (
<span className="text-denim-700 ml-2 text-xs"> <span className="ml-2 text-xs text-denim-700">
S{media.seasonId} E{media.episodeId} S{media.seasonId} E{media.episodeId}
</span> </span>
) : null} ) : null}

View File

@ -1,9 +1,9 @@
import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon";
import { Loading } from "components/layout/Loading";
import { MWMediaCaption, MWMediaStream } from "providers";
import { ReactElement, useEffect, useRef, useState } from "react"; import { ReactElement, useEffect, useRef, useState } from "react";
import Hls from "hls.js"; import Hls from "hls.js";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
import { MWMediaCaption, MWMediaStream } from "@/providers";
export interface VideoPlayerProps { export interface VideoPlayerProps {
source: MWMediaStream; source: MWMediaStream;
@ -14,7 +14,7 @@ export interface VideoPlayerProps {
export function SkeletonVideoPlayer(props: { error?: boolean }) { export function SkeletonVideoPlayer(props: { error?: boolean }) {
return ( return (
<div className="bg-denim-200 flex aspect-video w-full items-center justify-center lg:rounded-xl"> <div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl">
{props.error ? ( {props.error ? (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<IconPatch icon={Icons.WARNING} className="text-red-400" /> <IconPatch icon={Icons.WARNING} className="text-red-400" />
@ -44,8 +44,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
// hls support // hls support
if (mustUseHls) { if (mustUseHls) {
if (!videoRef.current) if (!videoRef.current) return;
return;
if (!Hls.isSupported()) { if (!Hls.isSupported()) {
setLoading(false); setLoading(false);
@ -55,7 +54,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
const hls = new Hls(); const hls = new Hls();
if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) {
videoRef.current.src = props.source.url; videoRef.current.src = props.source.url;
return; return;
} }
@ -81,8 +80,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
<> <>
{skeletonUi} {skeletonUi}
<video <video
className={`bg-black w-full rounded-xl ${!showVideo ? "hidden" : "" className={`w-full rounded-xl bg-black ${!showVideo ? "hidden" : ""}`}
}`}
ref={videoRef} ref={videoRef}
onProgress={(e) => onProgress={(e) =>
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent) props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)

View File

@ -1,5 +1,5 @@
import { getEpisodeFromMedia, MWMedia } from "providers"; import { getEpisodeFromMedia, MWMedia } from "@/providers";
import { useWatchedContext, getWatchedFromPortable } from "state/watched"; import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
import { Episode } from "./EpisodeButton"; import { Episode } from "./EpisodeButton";
export interface WatchedEpisodeProps { export interface WatchedEpisodeProps {

View File

@ -1,5 +1,5 @@
import { MWMediaMeta } from "providers"; import { MWMediaMeta } from "@/providers";
import { useWatchedContext, getWatchedFromPortable } from "state/watched"; import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
import { MediaCard } from "./MediaCard"; import { MediaCard } from "./MediaCard";
export interface WatchedMediaCardProps { export interface WatchedMediaCardProps {

View File

@ -1,5 +1,5 @@
import { Icon, Icons } from "components/Icon";
import { Link as LinkRouter } from "react-router-dom"; import { Link as LinkRouter } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon";
interface IArrowLinkPropsBase { interface IArrowLinkPropsBase {
linkText: string; linkText: string;
@ -26,7 +26,7 @@ export function ArrowLink(props: ArrowLinkProps) {
const isExternal = !!(props as IArrowLinkPropsExternal).url; const isExternal = !!(props as IArrowLinkPropsExternal).url;
const isInternal = !!(props as IArrowLinkPropsInternal).to; const isInternal = !!(props as IArrowLinkPropsInternal).to;
const content = ( const content = (
<span className="text-bink-600 hover:text-bink-700 group inline-flex cursor-pointer items-center space-x-1 font-bold active:scale-95 mt-1 pr-1"> <span className="group mt-1 inline-flex cursor-pointer items-center space-x-1 pr-1 font-bold text-bink-600 hover:text-bink-700 active:scale-95">
{direction === "left" ? ( {direction === "left" ? (
<span className="text-xl transition-transform group-hover:-translate-x-1"> <span className="text-xl transition-transform group-hover:-translate-x-1">
<Icon icon={Icons.ARROW_LEFT} /> <Icon icon={Icons.ARROW_LEFT} />
@ -45,7 +45,9 @@ export function ArrowLink(props: ArrowLinkProps) {
return <a href={(props as IArrowLinkPropsExternal).url}>{content}</a>; return <a href={(props as IArrowLinkPropsExternal).url}>{content}</a>;
if (isInternal) if (isInternal)
return ( return (
<LinkRouter to={(props as IArrowLinkPropsInternal).to}>{content}</LinkRouter> <LinkRouter to={(props as IArrowLinkPropsInternal).to}>
{content}
</LinkRouter>
); );
return ( return (
<span onClick={() => props.onClick && props.onClick()}>{content}</span> <span onClick={() => props.onClick && props.onClick()}>{content}</span>

View File

@ -1,6 +1,6 @@
import { MWPortableMedia } from "providers";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { MWPortableMedia } from "@/providers";
export function deserializePortableMedia(media: string): MWPortableMedia { export function deserializePortableMedia(media: string): MWPortableMedia {
return JSON.parse(atob(decodeURIComponent(media))); return JSON.parse(atob(decodeURIComponent(media)));

View File

@ -1,6 +1,6 @@
import { MWMediaType, MWQuery } from "providers";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { generatePath, useHistory, useRouteMatch } from "react-router-dom"; import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
import { MWMediaType, MWQuery } from "@/providers";
export function useSearchQuery(): [ export function useSearchQuery(): [
MWQuery, MWQuery,

View File

@ -2,7 +2,7 @@ import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
import "./index.css"; import "./index.css";
import { ErrorBoundary } from "components/layout/ErrorBoundary"; import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
import App from "./App"; import App from "./App";
ReactDOM.render( ReactDOM.render(

View File

@ -4,10 +4,10 @@ import {
MWPortableMedia, MWPortableMedia,
MWMediaStream, MWMediaStream,
MWQuery, MWQuery,
MWProviderMediaResult MWProviderMediaResult,
} from "providers/types"; } from "@/providers/types";
import { CORS_PROXY_URL } from "mw_constants"; import { CORS_PROXY_URL } from "@/mw_constants";
export const flixhqProvider: MWMediaProvider = { export const flixhqProvider: MWMediaProvider = {
id: "flixhq", id: "flixhq",
@ -15,9 +15,13 @@ export const flixhqProvider: MWMediaProvider = {
type: [MWMediaType.MOVIE], type: [MWMediaType.MOVIE],
displayName: "flixhq", displayName: "flixhq",
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> { async getMediaFromPortable(
media: MWPortableMedia
): Promise<MWProviderMediaResult> {
const searchRes = await fetch( const searchRes = await fetch(
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(media.mediaId)}` `${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(
media.mediaId
)}`
).then((d) => d.json()); ).then((d) => d.json());
return { return {
@ -29,39 +33,49 @@ export const flixhqProvider: MWMediaProvider = {
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> { async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
const searchRes = await fetch( const searchRes = await fetch(
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/${encodeURIComponent(query.searchQuery)}` `${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/${encodeURIComponent(
query.searchQuery
)}`
).then((d) => d.json()); ).then((d) => d.json());
const results: MWProviderMediaResult[] = (searchRes || []).results.map((item: any) => ({ const results: MWProviderMediaResult[] = (searchRes || []).results.map(
(item: any) => ({
title: item.title, title: item.title,
year: item.releaseDate, year: item.releaseDate,
mediaId: item.id, mediaId: item.id,
type: MWMediaType.MOVIE, type: MWMediaType.MOVIE,
})); })
);
return results; return results;
}, },
async getStream(media: MWPortableMedia): Promise<MWMediaStream> { async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
const searchRes = await fetch( const searchRes = await fetch(
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(media.mediaId)}` `${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(
media.mediaId
)}`
).then((d) => d.json()); ).then((d) => d.json());
const params = new URLSearchParams({ const params = new URLSearchParams({
episodeId: searchRes.episodes[0].id, episodeId: searchRes.episodes[0].id,
mediaId: media.mediaId mediaId: media.mediaId,
}) });
const watchRes = await fetch( const watchRes = await fetch(
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/watch?${encodeURIComponent(params.toString())}` `${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/watch?${encodeURIComponent(
params.toString()
)}`
).then((d) => d.json()); ).then((d) => d.json());
const source = watchRes.sources.reduce((p: any, c: any) => (c.quality > p.quality) ? c : p); const source = watchRes.sources.reduce((p: any, c: any) =>
c.quality > p.quality ? c : p
);
return { return {
url: source.url, url: source.url,
type: source.isM3U8 ? "m3u8" : "mp4", type: source.isM3U8 ? "m3u8" : "mp4",
captions: [] captions: [],
} as MWMediaStream; } as MWMediaStream;
}, },
}; };

View File

@ -1,15 +1,15 @@
import { unpack } from "unpacker";
import CryptoJS from "crypto-js";
import { import {
MWMediaProvider, MWMediaProvider,
MWMediaType, MWMediaType,
MWPortableMedia, MWPortableMedia,
MWMediaStream, MWMediaStream,
MWQuery, MWQuery,
MWProviderMediaResult MWProviderMediaResult,
} from "providers/types"; } from "@/providers/types";
import { CORS_PROXY_URL } from "mw_constants"; import { CORS_PROXY_URL } from "@/mw_constants";
import { unpack } from "unpacker";
import CryptoJS from "crypto-js";
const format = { const format = {
stringify: (cipher: any) => { stringify: (cipher: any) => {
@ -34,7 +34,7 @@ const format = {
salt, salt,
}); });
return cipher; return cipher;
} },
}; };
export const gDrivePlayerScraper: MWMediaProvider = { export const gDrivePlayerScraper: MWMediaProvider = {
@ -43,8 +43,12 @@ export const gDrivePlayerScraper: MWMediaProvider = {
type: [MWMediaType.MOVIE], type: [MWMediaType.MOVIE],
displayName: "gdriveplayer", displayName: "gdriveplayer",
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> { async getMediaFromPortable(
const res = await fetch(`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/imdb/${media.mediaId}`).then((d) => d.json()); media: MWPortableMedia
): Promise<MWProviderMediaResult> {
const res = await fetch(
`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/imdb/${media.mediaId}`
).then((d) => d.json());
return { return {
...media, ...media,
@ -54,19 +58,25 @@ export const gDrivePlayerScraper: MWMediaProvider = {
}, },
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> { async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
const searchRes = await fetch(`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/movie/search?title=${query.searchQuery}`).then((d) => d.json()); const searchRes = await fetch(
`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/movie/search?title=${query.searchQuery}`
).then((d) => d.json());
const results: MWProviderMediaResult[] = (searchRes || []).map((item: any) => ({ const results: MWProviderMediaResult[] = (searchRes || []).map(
(item: any) => ({
title: item.title, title: item.title,
year: item.year, year: item.year,
mediaId: item.imdb, mediaId: item.imdb,
})); })
);
return results; return results;
}, },
async getStream(media: MWPortableMedia): Promise<MWMediaStream> { async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
const streamRes = await fetch(`${CORS_PROXY_URL}https://database.gdriveplayer.us/player.php?imdb=${media.mediaId}`).then((d) => d.text()); const streamRes = await fetch(
`${CORS_PROXY_URL}https://database.gdriveplayer.us/player.php?imdb=${media.mediaId}`
).then((d) => d.text());
const page = new DOMParser().parseFromString(streamRes, "text/html"); const page = new DOMParser().parseFromString(streamRes, "text/html");
const script: HTMLElement | undefined = Array.from( const script: HTMLElement | undefined = Array.from(
@ -78,10 +88,29 @@ export const gDrivePlayerScraper: MWMediaProvider = {
} }
/// NOTE: this code requires re-write, it's not safe /// NOTE: this code requires re-write, it's not safe
const data = unpack(script.textContent).split("var data=\\'")[1].split("\\'")[0].replace(/\\/g, ""); const data = unpack(script.textContent)
const decryptedData = unpack(CryptoJS.AES.decrypt(data, "alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt", { format }).toString(CryptoJS.enc.Utf8)); .split("var data=\\'")[1]
.split("\\'")[0]
.replace(/\\/g, "");
const decryptedData = unpack(
CryptoJS.AES.decrypt(
data,
"alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt",
{ format }
).toString(CryptoJS.enc.Utf8)
);
// eslint-disable-next-line // eslint-disable-next-line
const sources = JSON.parse(JSON.stringify(eval(decryptedData.split("sources:")[1].split(",image")[0].replace(/\\/g, "").replace(/document\.referrer/g, "\"\"")))); const sources = JSON.parse(
JSON.stringify(
eval(
decryptedData
.split("sources:")[1]
.split(",image")[0]
.replace(/\\/g, "")
.replace(/document\.referrer/g, '""')
)
)
);
const source = sources[sources.length - 1]; const source = sources[sources.length - 1];
/// END /// END

View File

@ -1,15 +1,15 @@
import { unpack } from "unpacker";
import json5 from "json5";
import { import {
MWMediaProvider, MWMediaProvider,
MWMediaType, MWMediaType,
MWPortableMedia, MWPortableMedia,
MWMediaStream, MWMediaStream,
MWQuery, MWQuery,
MWProviderMediaResult MWProviderMediaResult,
} from "providers/types"; } from "@/providers/types";
import { CORS_PROXY_URL, OMDB_API_KEY } from "mw_constants"; import { CORS_PROXY_URL, OMDB_API_KEY } from "@/mw_constants";
import { unpack } from "unpacker";
import json5 from "json5";
export const gomostreamScraper: MWMediaProvider = { export const gomostreamScraper: MWMediaProvider = {
id: "gomostream", id: "gomostream",
@ -17,21 +17,25 @@ export const gomostreamScraper: MWMediaProvider = {
type: [MWMediaType.MOVIE], type: [MWMediaType.MOVIE],
displayName: "gomostream", displayName: "gomostream",
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> { async getMediaFromPortable(
media: MWPortableMedia
): Promise<MWProviderMediaResult> {
const params = new URLSearchParams({ const params = new URLSearchParams({
apikey: OMDB_API_KEY, apikey: OMDB_API_KEY,
i: media.mediaId, i: media.mediaId,
type: media.mediaType type: media.mediaType,
}); });
const res = await fetch( const res = await fetch(
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(params.toString())}`, `${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(
).then(d => d.json()) params.toString()
)}`
).then((d) => d.json());
return { return {
...media, ...media,
title: res.Title, title: res.Title,
year: res.Year year: res.Year,
} as MWProviderMediaResult; } as MWProviderMediaResult;
}, },
@ -41,51 +45,69 @@ export const gomostreamScraper: MWMediaProvider = {
const params = new URLSearchParams({ const params = new URLSearchParams({
apikey: OMDB_API_KEY, apikey: OMDB_API_KEY,
s: term, s: term,
type: query.type type: query.type,
}); });
const searchRes = await fetch( const searchRes = await fetch(
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(params.toString())}`, `${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(
).then(d => d.json()) params.toString()
)}`
).then((d) => d.json());
const results: MWProviderMediaResult[] = (searchRes.Search || []).map((d: any) => ({ const results: MWProviderMediaResult[] = (searchRes.Search || []).map(
(d: any) =>
({
title: d.Title, title: d.Title,
year: d.Year, year: d.Year,
mediaId: d.imdbID mediaId: d.imdbID,
} as MWProviderMediaResult)); } as MWProviderMediaResult)
);
return results; return results;
}, },
async getStream(media: MWPortableMedia): Promise<MWMediaStream> { async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
const type = media.mediaType === MWMediaType.SERIES ? 'show' : media.mediaType; const type =
const res1 = await fetch(`${CORS_PROXY_URL}https://gomo.to/${type}/${media.mediaId}`).then((d) => d.text()); media.mediaType === MWMediaType.SERIES ? "show" : media.mediaType;
if (res1 === "Movie not available." || res1 === "Episode not available.") throw new Error(res1); const res1 = await fetch(
`${CORS_PROXY_URL}https://gomo.to/${type}/${media.mediaId}`
).then((d) => d.text());
if (res1 === "Movie not available." || res1 === "Episode not available.")
throw new Error(res1);
const tc = res1.match(/var tc = '(.+)';/)?.[1] || ""; const tc = res1.match(/var tc = '(.+)';/)?.[1] || "";
const _token = res1.match(/"_token": "(.+)",/)?.[1] || ""; const _token = res1.match(/"_token": "(.+)",/)?.[1] || "";
const fd = new FormData() const fd = new FormData();
fd.append('tokenCode', tc) fd.append("tokenCode", tc);
fd.append('_token', _token) fd.append("_token", _token);
const src = await fetch(`${CORS_PROXY_URL}https://gomo.to/decoding_v3.php`, { const src = await fetch(
`${CORS_PROXY_URL}https://gomo.to/decoding_v3.php`,
{
method: "POST", method: "POST",
body: fd, body: fd,
headers: { headers: {
'x-token': `${tc.slice(5, 13).split("").reverse().join("")}13574199` "x-token": `${tc.slice(5, 13).split("").reverse().join("")}13574199`,
},
} }
}).then((d) => d.json()); ).then((d) => d.json());
const embeds = src.filter((url: string) => url.includes('gomo.to')); const embeds = src.filter((url: string) => url.includes("gomo.to"));
// maybe try all embeds in the future // maybe try all embeds in the future
const embedUrl = embeds[1]; const embedUrl = embeds[1];
const res2 = await fetch(`${CORS_PROXY_URL}${embedUrl}`).then((d) => d.text()); const res2 = await fetch(`${CORS_PROXY_URL}${embedUrl}`).then((d) =>
d.text()
);
const res2DOM = new DOMParser().parseFromString(res2, "text/html"); const res2DOM = new DOMParser().parseFromString(res2, "text/html");
if (res2DOM.body.innerText === "File was deleted") throw new Error("File was deleted"); if (res2DOM.body.innerText === "File was deleted")
throw new Error("File was deleted");
const script = Array.from(res2DOM.querySelectorAll("script")).find((s: HTMLScriptElement) => s.innerHTML.includes("eval(function(p,a,c,k,e,d"))?.innerHTML; const script = Array.from(res2DOM.querySelectorAll("script")).find(
if (!script) throw new Error("Could not get packed data") (s: HTMLScriptElement) =>
s.innerHTML.includes("eval(function(p,a,c,k,e,d")
)?.innerHTML;
if (!script) throw new Error("Could not get packed data");
const unpacked = unpack(script); const unpacked = unpack(script);
const rawSources = /sources:(\[.*?\])/.exec(unpacked); const rawSources = /sources:(\[.*?\])/.exec(unpacked);
@ -94,9 +116,10 @@ export const gomostreamScraper: MWMediaProvider = {
const sources = json5.parse(rawSources[1]); const sources = json5.parse(rawSources[1]);
const streamUrl = sources[0].file; const streamUrl = sources[0].file;
const streamType = streamUrl.split('.').at(-1); const streamType = streamUrl.split(".").at(-1);
if (streamType !== "mp4" && streamType !== "m3u8") throw new Error("Unsupported stream type"); if (streamType !== "mp4" && streamType !== "m3u8")
throw new Error("Unsupported stream type");
return { url: streamUrl, type: streamType, captions: [] }; return { url: streamUrl, type: streamType, captions: [] };
} },
}; };

View File

@ -1,6 +1,10 @@
// this is derived from https://github.com/recloudstream/cloudstream-extensions // this is derived from https://github.com/recloudstream/cloudstream-extensions
// for more info please check the LICENSE file in the same directory // for more info please check the LICENSE file in the same directory
import { customAlphabet } from "nanoid";
import toWebVTT from "srt-webvtt";
import CryptoJS from "crypto-js";
import { CORS_PROXY_URL, TMDB_API_KEY } from "@/mw_constants";
import { import {
MWMediaProvider, MWMediaProvider,
MWMediaType, MWMediaType,
@ -9,11 +13,7 @@ import {
MWQuery, MWQuery,
MWMediaSeasons, MWMediaSeasons,
MWProviderMediaResult, MWProviderMediaResult,
} from "providers/types"; } from "@/providers/types";
import { CORS_PROXY_URL, TMDB_API_KEY } from "mw_constants";
import { customAlphabet } from "nanoid";
import toWebVTT from "srt-webvtt";
import CryptoJS from "crypto-js";
const nanoid = customAlphabet("0123456789abcdef", 32); const nanoid = customAlphabet("0123456789abcdef", 32);
@ -41,7 +41,7 @@ const crypto = {
getVerify(str: string, str2: string, str3: string) { getVerify(str: string, str2: string, str3: string) {
if (str) { if (str) {
return CryptoJS.MD5( return CryptoJS.MD5(
CryptoJS.MD5(str2).toString() + str3 + str, CryptoJS.MD5(str2).toString() + str3 + str
).toString(); ).toString();
} }
return null; return null;
@ -66,7 +66,7 @@ const get = (data: object, altApi = false) => {
JSON.stringify({ JSON.stringify({
...defaultData, ...defaultData,
...data, ...data,
}), })
); );
const appKeyHash = CryptoJS.MD5(appKey).toString(); const appKeyHash = CryptoJS.MD5(appKey).toString();
const verify = crypto.getVerify(encryptedData, appKey, key); const verify = crypto.getVerify(encryptedData, appKey, key);
@ -102,7 +102,7 @@ export const superStreamScraper: MWMediaProvider = {
displayName: "SuperStream", displayName: "SuperStream",
async getMediaFromPortable( async getMediaFromPortable(
media: MWPortableMedia, media: MWPortableMedia
): Promise<MWProviderMediaResult> { ): Promise<MWProviderMediaResult> {
let apiQuery: any; let apiQuery: any;
if (media.mediaType === MWMediaType.SERIES) { if (media.mediaType === MWMediaType.SERIES) {
@ -174,10 +174,18 @@ export const superStreamScraper: MWMediaProvider = {
}; };
const mediaRes = (await get(apiQuery).then((r) => r.json())).data; const mediaRes = (await get(apiQuery).then((r) => r.json())).data;
const hdQuality = const hdQuality =
mediaRes.list.find((quality: any) => (quality.quality === "1080p" && quality.path)) ?? mediaRes.list.find(
mediaRes.list.find((quality: any) => (quality.quality === "720p" && quality.path)) ?? (quality: any) => quality.quality === "1080p" && quality.path
mediaRes.list.find((quality: any) => (quality.quality === "480p" && quality.path)) ?? ) ??
mediaRes.list.find((quality: any) => (quality.quality === "360p" && quality.path)); mediaRes.list.find(
(quality: any) => quality.quality === "720p" && quality.path
) ??
mediaRes.list.find(
(quality: any) => quality.quality === "480p" && quality.path
) ??
mediaRes.list.find(
(quality: any) => quality.quality === "360p" && quality.path
);
if (!hdQuality) throw new Error("No quality could be found."); if (!hdQuality) throw new Error("No quality could be found.");
@ -192,7 +200,7 @@ export const superStreamScraper: MWMediaProvider = {
const mappedCaptions = await Promise.all( const mappedCaptions = await Promise.all(
subtitleRes.list.map(async (subtitle: any) => { subtitleRes.list.map(async (subtitle: any) => {
const captionBlob = await fetch( const captionBlob = await fetch(
`${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`, `${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`
).then((captionRes) => captionRes.blob()); // cross-origin bypass ).then((captionRes) => captionRes.blob()); // cross-origin bypass
const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable
return { return {
@ -200,7 +208,7 @@ export const superStreamScraper: MWMediaProvider = {
url: captionUrl, url: captionUrl,
label: subtitle.language, label: subtitle.language,
}; };
}), })
); );
return { url: hdQuality.path, type: "mp4", captions: mappedCaptions }; return { url: hdQuality.path, type: "mp4", captions: mappedCaptions };
@ -217,10 +225,18 @@ export const superStreamScraper: MWMediaProvider = {
}; };
const mediaRes = (await get(apiQuery).then((r) => r.json())).data; const mediaRes = (await get(apiQuery).then((r) => r.json())).data;
const hdQuality = const hdQuality =
mediaRes.list.find((quality: any) => (quality.quality === "1080p" && quality.path)) ?? mediaRes.list.find(
mediaRes.list.find((quality: any) => (quality.quality === "720p" && quality.path)) ?? (quality: any) => quality.quality === "1080p" && quality.path
mediaRes.list.find((quality: any) => (quality.quality === "480p" && quality.path)) ?? ) ??
mediaRes.list.find((quality: any) => (quality.quality === "360p" && quality.path)); mediaRes.list.find(
(quality: any) => quality.quality === "720p" && quality.path
) ??
mediaRes.list.find(
(quality: any) => quality.quality === "480p" && quality.path
) ??
mediaRes.list.find(
(quality: any) => quality.quality === "360p" && quality.path
);
if (!hdQuality) throw new Error("No quality could be found."); if (!hdQuality) throw new Error("No quality could be found.");
@ -237,7 +253,7 @@ export const superStreamScraper: MWMediaProvider = {
const mappedCaptions = await Promise.all( const mappedCaptions = await Promise.all(
subtitleRes.list.map(async (subtitle: any) => { subtitleRes.list.map(async (subtitle: any) => {
const captionBlob = await fetch( const captionBlob = await fetch(
`${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`, `${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`
).then((captionRes) => captionRes.blob()); // cross-origin bypass ).then((captionRes) => captionRes.blob()); // cross-origin bypass
const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable
return { return {
@ -245,13 +261,13 @@ export const superStreamScraper: MWMediaProvider = {
url: captionUrl, url: captionUrl,
label: subtitle.language, label: subtitle.language,
}; };
}), })
); );
return { url: hdQuality.path, type: "mp4", captions: mappedCaptions }; return { url: hdQuality.path, type: "mp4", captions: mappedCaptions };
}, },
async getSeasonDataFromMedia( async getSeasonDataFromMedia(
media: MWPortableMedia, media: MWPortableMedia
): Promise<MWMediaSeasons> { ): Promise<MWMediaSeasons> {
const apiQuery = { const apiQuery = {
module: "TV_detail_1", module: "TV_detail_1",
@ -261,11 +277,11 @@ export const superStreamScraper: MWMediaProvider = {
const detailRes = (await get(apiQuery, true).then((r) => r.json())).data; const detailRes = (await get(apiQuery, true).then((r) => r.json())).data;
const firstSearchResult = ( const firstSearchResult = (
await fetch( await fetch(
`https://api.themoviedb.org/3/search/tv?api_key=${TMDB_API_KEY}&language=en-US&page=1&query=${detailRes.title}&include_adult=false`, `https://api.themoviedb.org/3/search/tv?api_key=${TMDB_API_KEY}&language=en-US&page=1&query=${detailRes.title}&include_adult=false`
).then((r) => r.json()) ).then((r) => r.json())
).results[0]; ).results[0];
const showDetails = await fetch( const showDetails = await fetch(
`https://api.themoviedb.org/3/tv/${firstSearchResult.id}?api_key=${TMDB_API_KEY}`, `https://api.themoviedb.org/3/tv/${firstSearchResult.id}?api_key=${TMDB_API_KEY}`
).then((r) => r.json()); ).then((r) => r.json());
return { return {
@ -279,7 +295,7 @@ export const superStreamScraper: MWMediaProvider = {
sort: epNum + 1, sort: epNum + 1,
id: (epNum + 1).toString(), id: (epNum + 1).toString(),
episodeNumber: epNum + 1, episodeNumber: epNum + 1,
}), })
), ),
})), })),
}; };

View File

@ -5,17 +5,17 @@ import {
MWMediaStream, MWMediaStream,
MWQuery, MWQuery,
MWMediaSeasons, MWMediaSeasons,
MWProviderMediaResult MWProviderMediaResult,
} from "providers/types"; } from "@/providers/types";
import { import {
searchTheFlix, searchTheFlix,
getDataFromSearch, getDataFromSearch,
turnDataIntoMedia, turnDataIntoMedia,
} from "providers/list/theflix/search"; } from "@/providers/list/theflix/search";
import { getDataFromPortableSearch } from "providers/list/theflix/portableToMedia"; import { getDataFromPortableSearch } from "@/providers/list/theflix/portableToMedia";
import { CORS_PROXY_URL } from "mw_constants"; import { CORS_PROXY_URL } from "@/mw_constants";
export const theFlixScraper: MWMediaProvider = { export const theFlixScraper: MWMediaProvider = {
id: "theflix", id: "theflix",

View File

@ -1,5 +1,5 @@
import { CORS_PROXY_URL } from "mw_constants"; import { CORS_PROXY_URL } from "@/mw_constants";
import { MWMediaType, MWPortableMedia } from "providers/types"; import { MWMediaType, MWPortableMedia } from "@/providers/types";
const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => { const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => {
if (media.mediaType === MWMediaType.MOVIE) { if (media.mediaType === MWMediaType.MOVIE) {

View File

@ -1,5 +1,5 @@
import { CORS_PROXY_URL } from "mw_constants"; import { CORS_PROXY_URL } from "@/mw_constants";
import { MWMediaType, MWProviderMediaResult, MWQuery } from "providers"; import { MWMediaType, MWProviderMediaResult, MWQuery } from "@/providers";
const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) => const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) =>
`https://theflix.to/${type}/trending?${params}`; `https://theflix.to/${type}/trending?${params}`;

View File

@ -5,10 +5,10 @@ import {
MWMediaStream, MWMediaStream,
MWQuery, MWQuery,
MWProviderMediaResult, MWProviderMediaResult,
MWMediaCaption MWMediaCaption,
} from "providers/types"; } from "@/providers/types";
import { CORS_PROXY_URL } from "mw_constants"; import { CORS_PROXY_URL } from "@/mw_constants";
export const xemovieScraper: MWMediaProvider = { export const xemovieScraper: MWMediaProvider = {
id: "xemovie", id: "xemovie",
@ -16,15 +16,21 @@ export const xemovieScraper: MWMediaProvider = {
type: [MWMediaType.MOVIE], type: [MWMediaType.MOVIE],
displayName: "xemovie", displayName: "xemovie",
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> { async getMediaFromPortable(
media: MWPortableMedia
): Promise<MWProviderMediaResult> {
const res = await fetch( const res = await fetch(
`${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`, `${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`
).then(d => d.text()); ).then((d) => d.text());
const DOM = new DOMParser().parseFromString(res, "text/html"); const DOM = new DOMParser().parseFromString(res, "text/html");
const title = DOM.querySelector(".text-primary.text-lg.font-extrabold")?.textContent || ""; const title =
const year = DOM.querySelector("div.justify-between:nth-child(3) > div:nth-child(2)")?.textContent || ""; DOM.querySelector(".text-primary.text-lg.font-extrabold")?.textContent ||
"";
const year =
DOM.querySelector("div.justify-between:nth-child(3) > div:nth-child(2)")
?.textContent || "";
return { return {
...media, ...media,
@ -36,17 +42,24 @@ export const xemovieScraper: MWMediaProvider = {
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> { async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
const term = query.searchQuery.toLowerCase(); const term = query.searchQuery.toLowerCase();
const searchUrl = `${CORS_PROXY_URL}https://xemovie.co/search?q=${encodeURIComponent(term)}`; const searchUrl = `${CORS_PROXY_URL}https://xemovie.co/search?q=${encodeURIComponent(
term
)}`;
const searchRes = await fetch(searchUrl).then((d) => d.text()); const searchRes = await fetch(searchUrl).then((d) => d.text());
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(searchRes, "text/html"); const doc = parser.parseFromString(searchRes, "text/html");
const movieContainer = doc.querySelectorAll(".py-10")[0].querySelector(".grid"); const movieContainer = doc
.querySelectorAll(".py-10")[0]
.querySelector(".grid");
if (!movieContainer) return []; if (!movieContainer) return [];
const movieNodes = Array.from(movieContainer.querySelectorAll("a")).filter(link => !link.className); const movieNodes = Array.from(movieContainer.querySelectorAll("a")).filter(
(link) => !link.className
);
const results: MWProviderMediaResult[] = movieNodes.map((node) => { const results: MWProviderMediaResult[] = movieNodes
.map((node) => {
const parent = node.parentElement; const parent = node.parentElement;
if (!parent) return; if (!parent) return;
@ -56,48 +69,72 @@ export const xemovieScraper: MWMediaProvider = {
return { return {
title: parent.querySelector("div > div > a > h6")?.textContent, title: parent.querySelector("div > div > a > h6")?.textContent,
year: parent.querySelector("div.float-right")?.textContent, year: parent.querySelector("div.float-right")?.textContent,
mediaId: aElement.href.split('/').pop() || "", mediaId: aElement.href.split("/").pop() || "",
} };
}).filter((d): d is MWProviderMediaResult => !!d); })
.filter((d): d is MWProviderMediaResult => !!d);
return results; return results;
}, },
async getStream(media: MWPortableMedia): Promise<MWMediaStream> { async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
if (media.mediaType !== MWMediaType.MOVIE) throw new Error("Incorrect type") if (media.mediaType !== MWMediaType.MOVIE)
throw new Error("Incorrect type");
const url = `${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`; const url = `${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`;
let streamUrl = ""; let streamUrl = "";
const subtitles: MWMediaCaption[] = []; const subtitles: MWMediaCaption[] = [];
const res = await fetch(url).then(d => d.text()); const res = await fetch(url).then((d) => d.text());
const scripts = Array.from(new DOMParser().parseFromString(res, "text/html").querySelectorAll("script")); const scripts = Array.from(
new DOMParser()
.parseFromString(res, "text/html")
.querySelectorAll("script")
);
for (const script of scripts) { for (const script of scripts) {
if (!script.textContent) continue; if (!script.textContent) continue;
if (script.textContent.match(/https:\/\/[a-z][0-9]\.xemovie\.com/)) { if (script.textContent.match(/https:\/\/[a-z][0-9]\.xemovie\.com/)) {
const data = JSON.parse(JSON.stringify(eval(`(${script.textContent.replace("const data = ", "").split("};")[0]}})`))); const data = JSON.parse(
JSON.stringify(
eval(
`(${
script.textContent.replace("const data = ", "").split("};")[0]
}})`
)
)
);
streamUrl = data.playlist[0].file; streamUrl = data.playlist[0].file;
for (const [index, subtitleTrack] of data.playlist[0].tracks.entries()) { for (const [
index,
subtitleTrack,
] of data.playlist[0].tracks.entries()) {
const subtitleBlob = URL.createObjectURL( const subtitleBlob = URL.createObjectURL(
await fetch(`${CORS_PROXY_URL}${subtitleTrack.file}`).then((captionRes) => captionRes.blob()) await fetch(`${CORS_PROXY_URL}${subtitleTrack.file}`).then(
(captionRes) => captionRes.blob()
)
); // do this so no need for CORS errors ); // do this so no need for CORS errors
subtitles.push({ subtitles.push({
id: index, id: index,
url: subtitleBlob, url: subtitleBlob,
label: subtitleTrack.label label: subtitleTrack.label,
}) });
} }
} }
} }
const streamType = streamUrl.split('.').at(-1); const streamType = streamUrl.split(".").at(-1);
if (streamType !== "mp4" && streamType !== "m3u8") throw new Error("Unsupported stream type"); if (streamType !== "mp4" && streamType !== "m3u8")
throw new Error("Unsupported stream type");
return { url: streamUrl, type: streamType, captions: subtitles } as MWMediaStream; return {
} url: streamUrl,
type: streamType,
captions: subtitles,
} as MWMediaStream;
},
}; };

View File

@ -1,9 +1,11 @@
import { SimpleCache } from "utils/cache"; import { SimpleCache } from "@/utils/cache";
import { MWPortableMedia, MWMedia } from "providers"; import { MWPortableMedia, MWMedia } from "@/providers";
// cache // cache
const contentCache = new SimpleCache<MWPortableMedia, MWMedia>(); const contentCache = new SimpleCache<MWPortableMedia, MWMedia>();
contentCache.setCompare((a,b) => a.mediaId === b.mediaId && a.providerId === b.providerId); contentCache.setCompare(
(a, b) => a.mediaId === b.mediaId && a.providerId === b.providerId
);
contentCache.initialize(); contentCache.initialize();
export default contentCache; export default contentCache;

View File

@ -1,5 +1,5 @@
import { MWMediaType, MWMediaProviderMetadata } from "providers"; import { MWMediaType, MWMediaProviderMetadata } from "@/providers";
import { MWMedia, MWMediaEpisode, MWMediaSeason } from "providers/types"; import { MWMedia, MWMediaEpisode, MWMediaSeason } from "@/providers/types";
import { mediaProviders, mediaProvidersUnchecked } from "./providers"; import { mediaProviders, mediaProvidersUnchecked } from "./providers";
/* /*

View File

@ -1,10 +1,10 @@
import { theFlixScraper } from "providers/list/theflix"; import { theFlixScraper } from "@/providers/list/theflix";
import { gDrivePlayerScraper } from "providers/list/gdriveplayer"; import { gDrivePlayerScraper } from "@/providers/list/gdriveplayer";
import { MWWrappedMediaProvider, WrapProvider } from "providers/wrapper"; import { MWWrappedMediaProvider, WrapProvider } from "@/providers/wrapper";
import { gomostreamScraper } from "providers/list/gomostream"; import { gomostreamScraper } from "@/providers/list/gomostream";
import { xemovieScraper } from "providers/list/xemovie"; import { xemovieScraper } from "@/providers/list/xemovie";
import { flixhqProvider } from "providers/list/flixhq"; import { flixhqProvider } from "@/providers/list/flixhq";
import { superStreamScraper } from "providers/list/superstream"; import { superStreamScraper } from "@/providers/list/superstream";
export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [ export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [
WrapProvider(superStreamScraper), WrapProvider(superStreamScraper),

View File

@ -4,8 +4,8 @@ import {
MWMedia, MWMedia,
MWQuery, MWQuery,
convertMediaToPortable, convertMediaToPortable,
} from "providers"; } from "@/providers";
import { SimpleCache } from "utils/cache"; import { SimpleCache } from "@/utils/cache";
import { GetProvidersForType } from "./helpers"; import { GetProvidersForType } from "./helpers";
import contentCache from "./contentCache"; import contentCache from "./contentCache";

View File

@ -1,6 +1,10 @@
import { SimpleCache } from "utils/cache"; import { SimpleCache } from "@/utils/cache";
import { MWPortableMedia } from "providers"; import { MWPortableMedia } from "@/providers";
import { MWMediaSeasons, MWMediaType, MWMediaProviderSeries } from "providers/types"; import {
MWMediaSeasons,
MWMediaType,
MWMediaProviderSeries,
} from "@/providers/types";
import { getProviderFromId } from "./helpers"; import { getProviderFromId } from "./helpers";
// cache // cache
@ -23,7 +27,10 @@ export async function getSeasonDataFromMedia(
}; };
} }
if (!provider.type.includes(MWMediaType.SERIES) && !provider.type.includes(MWMediaType.ANIME)) { if (
!provider.type.includes(MWMediaType.SERIES) &&
!provider.type.includes(MWMediaType.ANIME)
) {
return { return {
seasons: [], seasons: [],
}; };

View File

@ -1,4 +1,3 @@
import { getProviderMetadata, MWMediaMeta } from "providers";
import { import {
createContext, createContext,
ReactNode, ReactNode,
@ -7,6 +6,7 @@ import {
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
import { getProviderMetadata, MWMediaMeta } from "@/providers";
import { BookmarkStore } from "./store"; import { BookmarkStore } from "./store";
interface BookmarkStoreData { interface BookmarkStoreData {

View File

@ -1,13 +1,13 @@
import { versionedStoreBuilder } from 'utils/storage'; import { versionedStoreBuilder } from "@/utils/storage";
export const BookmarkStore = versionedStoreBuilder() export const BookmarkStore = versionedStoreBuilder()
.setKey('mw-bookmarks') .setKey("mw-bookmarks")
.addVersion({ .addVersion({
version: 0, version: 0,
create() { create() {
return { return {
bookmarks: [] bookmarks: [],
} };
} },
}) })
.build() .build();

View File

@ -1,4 +1,3 @@
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "providers";
import React, { import React, {
createContext, createContext,
ReactNode, ReactNode,
@ -7,6 +6,7 @@ import React, {
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "@/providers";
import { VideoProgressStore } from "./store"; import { VideoProgressStore } from "./store";
interface WatchedStoreItem extends MWMediaMeta { interface WatchedStoreItem extends MWMediaMeta {
@ -38,7 +38,7 @@ export function getWatchedFromPortable(
} }
const WatchedContext = createContext<WatchedStoreDataWrapper>({ const WatchedContext = createContext<WatchedStoreDataWrapper>({
updateProgress: () => { }, updateProgress: () => {},
getFilteredWatched: () => [], getFilteredWatched: () => [],
watched: { watched: {
items: [], items: [],

View File

@ -1,5 +1,5 @@
import { MWMediaType } from "providers"; import { MWMediaType } from "@/providers";
import { versionedStoreBuilder } from "utils/storage"; import { versionedStoreBuilder } from "@/utils/storage";
import { WatchedStoreData } from "./context"; import { WatchedStoreData } from "./context";
export const VideoProgressStore = versionedStoreBuilder() export const VideoProgressStore = versionedStoreBuilder()
@ -12,35 +12,48 @@ export const VideoProgressStore = versionedStoreBuilder()
migrate(data: any) { migrate(data: any) {
const output: WatchedStoreData = { items: [] }; const output: WatchedStoreData = { items: [] };
if (!data || data.constructor !== Object) if (!data || data.constructor !== Object) return output;
return output;
Object.keys(data).forEach((scraperId) => { Object.keys(data).forEach((scraperId) => {
if (scraperId === "--version") return; if (scraperId === "--version") return;
if (scraperId === "save") return; if (scraperId === "save") return;
if (data[scraperId].movie && data[scraperId].movie.constructor === Object) { if (
data[scraperId].movie &&
data[scraperId].movie.constructor === Object
) {
Object.keys(data[scraperId].movie).forEach((movieId) => { Object.keys(data[scraperId].movie).forEach((movieId) => {
try { try {
output.items.push({ output.items.push({
mediaId: movieId.includes("player.php") ? movieId.split("player.php%3Fimdb%3D")[1] : movieId, mediaId: movieId.includes("player.php")
? movieId.split("player.php%3Fimdb%3D")[1]
: movieId,
mediaType: MWMediaType.MOVIE, mediaType: MWMediaType.MOVIE,
providerId: scraperId, providerId: scraperId,
title: data[scraperId].movie[movieId].full.meta.title, title: data[scraperId].movie[movieId].full.meta.title,
year: data[scraperId].movie[movieId].full.meta.year, year: data[scraperId].movie[movieId].full.meta.year,
progress: data[scraperId].movie[movieId].full.currentlyAt, progress: data[scraperId].movie[movieId].full.currentlyAt,
percentage: Math.round((data[scraperId].movie[movieId].full.currentlyAt / data[scraperId].movie[movieId].full.totalDuration) * 100) percentage: Math.round(
(data[scraperId].movie[movieId].full.currentlyAt /
data[scraperId].movie[movieId].full.totalDuration) *
100
),
}); });
} catch (err) { } catch (err) {
console.error(`Failed to migrate movie: ${scraperId}/${movieId}`, data[scraperId].movie[movieId]); console.error(
`Failed to migrate movie: ${scraperId}/${movieId}`,
data[scraperId].movie[movieId]
);
} }
}); });
} }
if (data[scraperId].show && data[scraperId].show.constructor === Object) { if (
data[scraperId].show &&
data[scraperId].show.constructor === Object
) {
Object.keys(data[scraperId].show).forEach((showId) => { Object.keys(data[scraperId].show).forEach((showId) => {
if (data[scraperId].show[showId].constructor !== Object) if (data[scraperId].show[showId].constructor !== Object) return;
return;
Object.keys(data[scraperId].show[showId]).forEach((episodeId) => { Object.keys(data[scraperId].show[showId]).forEach((episodeId) => {
try { try {
output.items.push({ output.items.push({
@ -49,13 +62,21 @@ export const VideoProgressStore = versionedStoreBuilder()
providerId: scraperId, providerId: scraperId,
title: data[scraperId].show[showId][episodeId].meta.title, title: data[scraperId].show[showId][episodeId].meta.title,
year: data[scraperId].show[showId][episodeId].meta.year, year: data[scraperId].show[showId][episodeId].meta.year,
percentage: Math.round((data[scraperId].show[showId][episodeId].currentlyAt / data[scraperId].show[showId][episodeId].totalDuration) * 100), percentage: Math.round(
(data[scraperId].show[showId][episodeId].currentlyAt /
data[scraperId].show[showId][episodeId].totalDuration) *
100
),
progress: data[scraperId].show[showId][episodeId].currentlyAt, progress: data[scraperId].show[showId][episodeId].currentlyAt,
episodeId: data[scraperId].show[showId][episodeId].show.episode, episodeId:
data[scraperId].show[showId][episodeId].show.episode,
seasonId: data[scraperId].show[showId][episodeId].show.season, seasonId: data[scraperId].show[showId][episodeId].show.season,
}); });
} catch (err) { } catch (err) {
console.error(`Failed to migrate series: ${scraperId}/${showId}/${episodeId}`, data[scraperId].show[showId][episodeId]); console.error(
`Failed to migrate series: ${scraperId}/${showId}/${episodeId}`,
data[scraperId].show[showId][episodeId]
);
} }
}); });
}); });

View File

@ -1,14 +1,19 @@
import { IconPatch } from "components/buttons/IconPatch"; import { ReactElement, useEffect, useState } from "react";
import { Icons } from "components/Icon"; import { useHistory } from "react-router-dom";
import { Navigation } from "components/layout/Navigation"; import { IconPatch } from "@/components/buttons/IconPatch";
import { Paper } from "components/layout/Paper"; import { Icons } from "@/components/Icon";
import { LoadingSeasons, Seasons } from "components/layout/Seasons"; import { Navigation } from "@/components/layout/Navigation";
import { SkeletonVideoPlayer, VideoPlayer } from "components/media/VideoPlayer"; import { Paper } from "@/components/layout/Paper";
import { ArrowLink } from "components/text/ArrowLink"; import { LoadingSeasons, Seasons } from "@/components/layout/Seasons";
import { DotList } from "components/text/DotList"; import {
import { Title } from "components/text/Title"; SkeletonVideoPlayer,
import { useLoading } from "hooks/useLoading"; VideoPlayer,
import { usePortableMedia } from "hooks/usePortableMedia"; } from "@/components/media/VideoPlayer";
import { ArrowLink } from "@/components/text/ArrowLink";
import { DotList } from "@/components/text/DotList";
import { Title } from "@/components/text/Title";
import { useLoading } from "@/hooks/useLoading";
import { usePortableMedia } from "@/hooks/usePortableMedia";
import { import {
MWPortableMedia, MWPortableMedia,
getStream, getStream,
@ -18,14 +23,12 @@ import {
getProviderFromId, getProviderFromId,
MWMediaProvider, MWMediaProvider,
MWMediaType, MWMediaType,
} from "providers"; } from "@/providers";
import { ReactElement, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { import {
getIfBookmarkedFromPortable, getIfBookmarkedFromPortable,
useBookmarkContext, useBookmarkContext,
} from "state/bookmark"; } from "@/state/bookmark";
import { getWatchedFromPortable, useWatchedContext } from "state/watched"; import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
import { NotFoundChecks } from "./notfound/NotFoundChecks"; import { NotFoundChecks } from "./notfound/NotFoundChecks";
interface StyledMediaViewProps { interface StyledMediaViewProps {
@ -106,10 +109,10 @@ function LoadingMediaFooter(props: { error?: boolean }) {
<Paper className="mt-5"> <Paper className="mt-5">
<div className="flex"> <div className="flex">
<div className="flex-1"> <div className="flex-1">
<div className="bg-denim-500 mb-2 h-4 w-48 rounded-full" /> <div className="mb-2 h-4 w-48 rounded-full bg-denim-500" />
<div> <div>
<span className="bg-denim-400 mr-4 inline-block h-2 w-12 rounded-full" /> <span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
<span className="bg-denim-400 mr-4 inline-block h-2 w-12 rounded-full" /> <span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
</div> </div>
{props.error ? ( {props.error ? (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">

View File

@ -1,23 +1,23 @@
import { WatchedMediaCard } from "components/media/WatchedMediaCard";
import { SearchBarInput } from "components/SearchBar";
import { MWMassProviderOutput, MWQuery, SearchProviders } from "providers";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { ThinContainer } from "components/layout/ThinContainer"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { SectionHeading } from "components/layout/SectionHeading"; import { SearchBarInput } from "@/components/SearchBar";
import { Icons } from "components/Icon"; import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers";
import { Loading } from "components/layout/Loading"; import { ThinContainer } from "@/components/layout/ThinContainer";
import { Tagline } from "components/text/Tagline"; import { SectionHeading } from "@/components/layout/SectionHeading";
import { Title } from "components/text/Title"; import { Icons } from "@/components/Icon";
import { useDebounce } from "hooks/useDebounce"; import { Loading } from "@/components/layout/Loading";
import { useLoading } from "hooks/useLoading"; import { Tagline } from "@/components/text/Tagline";
import { IconPatch } from "components/buttons/IconPatch"; import { Title } from "@/components/text/Title";
import { Navigation } from "components/layout/Navigation"; import { useDebounce } from "@/hooks/useDebounce";
import { useSearchQuery } from "hooks/useSearchQuery"; import { useLoading } from "@/hooks/useLoading";
import { useWatchedContext } from "state/watched/context"; import { IconPatch } from "@/components/buttons/IconPatch";
import { Navigation } from "@/components/layout/Navigation";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import { useWatchedContext } from "@/state/watched/context";
import { import {
getIfBookmarkedFromPortable, getIfBookmarkedFromPortable,
useBookmarkContext, useBookmarkContext,
} from "state/bookmark/context"; } from "@/state/bookmark/context";
function SearchLoading() { function SearchLoading() {
return <Loading className="my-24" text="Fetching your favourite shows..." />; return <Loading className="my-24" text="Fetching your favourite shows..." />;

View File

@ -1,5 +1,5 @@
import { getProviderMetadata, MWPortableMedia } from "providers";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { getProviderMetadata, MWPortableMedia } from "@/providers";
import { NotFoundMedia, NotFoundProvider } from "./NotFoundView"; import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
export interface NotFoundChecksProps { export interface NotFoundChecksProps {

View File

@ -1,9 +1,9 @@
import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon";
import { Navigation } from "components/layout/Navigation";
import { ArrowLink } from "components/text/ArrowLink";
import { Title } from "components/text/Title";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Navigation } from "@/components/layout/Navigation";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
function NotFoundWrapper(props: { children?: ReactNode }) { function NotFoundWrapper(props: { children?: ReactNode }) {
return ( return (
@ -21,7 +21,7 @@ export function NotFoundMedia() {
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center"> <div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
<IconPatch <IconPatch
icon={Icons.EYE_SLASH} icon={Icons.EYE_SLASH}
className="text-bink-600 mb-6 text-xl" className="mb-6 text-xl text-bink-600"
/> />
<Title>Couldn&apos;t find that media</Title> <Title>Couldn&apos;t find that media</Title>
<p className="mt-5 mb-12 max-w-sm"> <p className="mt-5 mb-12 max-w-sm">
@ -38,7 +38,7 @@ export function NotFoundProvider() {
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center"> <div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
<IconPatch <IconPatch
icon={Icons.EYE_SLASH} icon={Icons.EYE_SLASH}
className="text-bink-600 mb-6 text-xl" className="mb-6 text-xl text-bink-600"
/> />
<Title>This provider has been disabled</Title> <Title>This provider has been disabled</Title>
<p className="mt-5 mb-12 max-w-sm"> <p className="mt-5 mb-12 max-w-sm">
@ -55,7 +55,7 @@ export function NotFoundPage() {
<NotFoundWrapper> <NotFoundWrapper>
<IconPatch <IconPatch
icon={Icons.EYE_SLASH} icon={Icons.EYE_SLASH}
className="text-bink-600 mb-6 text-xl" className="mb-6 text-xl text-bink-600"
/> />
<Title>Couldn&apos;t find that page</Title> <Title>Couldn&apos;t find that page</Title>
<p className="mt-5 mb-12 max-w-sm"> <p className="mt-5 mb-12 max-w-sm">

View File

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ES2020",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -20,8 +16,10 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": "./src", "baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}, },
"include": [ "types": ["vite/client"]
"src" },
] "include": ["src"]
} }

View File

@ -6,13 +6,7 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
components: path.resolve(__dirname, "./src/components"), "@": path.resolve(__dirname, "./src"),
hooks: path.resolve(__dirname, "./src/hooks"),
state: path.resolve(__dirname, "./src/state"),
providers: path.resolve(__dirname, "./src/providers"),
views: path.resolve(__dirname, "./src/views"),
utils: path.resolve(__dirname, "./src/utils"),
mw_constants: path.resolve(__dirname, "./src/mw_constants"),
}, },
}, },
}); });