diff --git a/.eslintrc.js b/.eslintrc.js index c67da485..7710c4fd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,11 @@ module.exports = { project: "./tsconfig.json", tsconfigRootDir: "./", }, + settings: { + "import/resolver": { + typescript: {}, + }, + }, plugins: ["@typescript-eslint", "import"], rules: { "react/jsx-uses-react": "off", diff --git a/package.json b/package.json index 70ad7d64..7a1256a4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build": "vite build", "preview": "vite preview", "lint": "eslint --ext .tsx,.ts src", - "lint:strict": "eslint --ext .tsx,.ts --max-warnings 0 src", + "lint:fix": "eslint --fix --ext .tsx,.ts src", "lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src" }, "browserslist": { diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index a73ad138..84ab2957 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -57,5 +57,5 @@ export function Dropdown(props: DropdownProps) { )} - ) + ); } diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 3be9d0b5..df844c83 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { MWMediaType, MWQuery } from "@/providers"; import { useTranslation } from "react-i18next"; +import { MWMediaType, MWQuery } from "@/providers"; import { DropdownButton } from "./buttons/DropdownButton"; import { Icon, Icons } from "./Icon"; import { TextInputControl } from "./text-inputs/TextInputControl"; diff --git a/src/components/layout/Backdrop.tsx b/src/components/layout/Backdrop.tsx index 3ded3afc..57719e29 100644 --- a/src/components/layout/Backdrop.tsx +++ b/src/components/layout/Backdrop.tsx @@ -1,6 +1,6 @@ import React, { createRef, useEffect, useState } from "react"; -import { useFade } from "@/hooks/useFade"; import { createPortal } from "react-dom"; +import { useFade } from "@/hooks/useFade"; interface BackdropProps { onClick?: (e: MouseEvent) => void; diff --git a/src/components/layout/BrandPill.tsx b/src/components/layout/BrandPill.tsx index 82a037cb..cef38ab5 100644 --- a/src/components/layout/BrandPill.tsx +++ b/src/components/layout/BrandPill.tsx @@ -1,5 +1,5 @@ -import { Icon, Icons } from "@/components/Icon"; import { useTranslation } from "react-i18next"; +import { Icon, Icons } from "@/components/Icon"; export function BrandPill(props: { clickable?: boolean }) { const { t } = useTranslation(); diff --git a/src/components/layout/Loading.tsx b/src/components/layout/Loading.tsx index 7af05dfe..cff6a503 100644 --- a/src/components/layout/Loading.tsx +++ b/src/components/layout/Loading.tsx @@ -8,10 +8,10 @@ export function Loading(props: LoadingProps) {
-
-
-
-
+
+
+
+
{props.text && props.text.length ? (

{props.text}

diff --git a/src/components/layout/Paper.tsx b/src/components/layout/Paper.tsx index e2e8475b..a87895ae 100644 --- a/src/components/layout/Paper.tsx +++ b/src/components/layout/Paper.tsx @@ -1,14 +1,16 @@ import { ReactNode } from "react"; export interface PaperProps { - children?: ReactNode, - className?: string, + children?: ReactNode; + className?: string; } export function Paper(props: PaperProps) { return ( -
+
{props.children}
- ) + ); } diff --git a/src/components/layout/Seasons.tsx b/src/components/layout/Seasons.tsx index b6baab05..f2416372 100644 --- a/src/components/layout/Seasons.tsx +++ b/src/components/layout/Seasons.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Dropdown, OptionItem } from "@/components/Dropdown"; import { Icons } from "@/components/Icon"; @@ -14,7 +15,6 @@ import { MWPortableMedia, } from "@/providers"; import { getSeasonDataFromMedia } from "@/providers/methods/seasons"; -import { useTranslation } from "react-i18next"; export interface SeasonsProps { media: MWMedia; @@ -37,7 +37,7 @@ export function LoadingSeasons(props: { error?: boolean }) { ) : (
-

{t('seasons.failed')}

+

{t("seasons.failed")}

)}
@@ -75,7 +75,7 @@ export function Seasons(props: SeasonsProps) { const mapSeason = (season: MWMediaSeason) => ({ id: season.id, - name: season.title || `${t('seasons.season', { season: season.sort })}`, + name: season.title || `${t("seasons.season", { season: season.sort })}`, }); const options = seasons.seasons.map(mapSeason); diff --git a/src/components/media/EpisodeButton.tsx b/src/components/media/EpisodeButton.tsx index c4e851d0..f3e4375c 100644 --- a/src/components/media/EpisodeButton.tsx +++ b/src/components/media/EpisodeButton.tsx @@ -9,12 +9,12 @@ export function Episode(props: EpisodeProps) { return (
+

{props.content.map((item, index) => ( {index !== 0 ? ( diff --git a/src/components/text/Link.tsx b/src/components/text/Link.tsx index 7505f41c..1451114e 100644 --- a/src/components/text/Link.tsx +++ b/src/components/text/Link.tsx @@ -16,22 +16,27 @@ interface ILinkPropsInternal extends ILinkPropsBase { to: string; } -type LinkProps = - | ILinkPropsExternal - | ILinkPropsInternal - | ILinkPropsBase; +type LinkProps = ILinkPropsExternal | ILinkPropsInternal | ILinkPropsBase; export function Link(props: LinkProps) { const isExternal = !!(props as ILinkPropsExternal).url; const isInternal = !!(props as ILinkPropsInternal).to; const content = ( - + {props.children} ); if (isExternal) - return {content}; + return ( + + {content} + + ); if (isInternal) return ( {content} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 509337b6..fdc9b6db 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -4,17 +4,14 @@ export function useDebounce(value: T, delay: number): T { // State and setters for debounced value const [debouncedValue, setDebouncedValue] = useState(value); - useEffect( - () => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - return () => { - clearTimeout(handler); - }; - }, - [value, delay] - ); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, [value, delay]); return debouncedValue; } diff --git a/src/hooks/useFade.ts b/src/hooks/useFade.ts index e03413fd..58438acf 100644 --- a/src/hooks/useFade.ts +++ b/src/hooks/useFade.ts @@ -1,7 +1,9 @@ import React, { useEffect, useState } from "react"; -import './useFade.css' +import "./useFade.css"; -export const useFade = (initial = false): [boolean, React.Dispatch>, any] => { +export const useFade = ( + initial = false +): [boolean, React.Dispatch>, any] => { const [show, setShow] = useState(initial); const [isVisible, setVisible] = useState(show); @@ -20,7 +22,7 @@ export const useFade = (initial = false): [boolean, React.Dispatch; - -export interface MWQuery { - searchQuery: string; - type: MWMediaType; -} - -export interface MWMediaProviderBase { - id: string; // id of provider, must be unique - enabled: boolean; - type: MWMediaType[]; - displayName: string; - - getMediaFromPortable(media: MWPortableMedia): Promise; - searchForMedia(query: MWQuery): Promise; - getStream(media: MWPortableMedia): Promise; - getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise; -} - -export type MWMediaProviderSeries = MWMediaProviderBase & { - getSeasonDataFromMedia: (media: MWPortableMedia) => Promise; -}; - -export type MWMediaProvider = MWMediaProviderBase; - -export interface MWMediaProviderMetadata { - exists: boolean; - id?: string; - enabled: boolean; - type: MWMediaType[]; - provider?: MWMediaProvider; -} - -export interface MWMassProviderOutput { - providers: { - id: string; - success: boolean; - }[]; - results: MWMedia[]; - stats: { - total: number; - failed: number; - succeeded: number; - }; -} +export enum MWMediaType { + MOVIE = "movie", + SERIES = "series", + ANIME = "anime", +} + +export interface MWPortableMedia { + mediaId: string; + mediaType: MWMediaType; + providerId: string; + seasonId?: string; + episodeId?: string; +} + +export type MWMediaStreamType = "m3u8" | "mp4"; +export interface MWMediaCaption { + id: string; + url: string; + label: string; +} +export interface MWMediaStream { + url: string; + type: MWMediaStreamType; + captions: MWMediaCaption[]; +} + +export interface MWMediaMeta extends MWPortableMedia { + title: string; + year: string; + seasonCount?: number; +} + +export interface MWMediaEpisode { + sort: number; + id: string; + title: string; +} +export interface MWMediaSeason { + sort: number; + id: string; + title?: string; + type: "season" | "special"; + episodes: MWMediaEpisode[]; +} +export interface MWMediaSeasons { + seasons: MWMediaSeason[]; +} + +export interface MWMedia extends MWMediaMeta { + seriesData?: MWMediaSeasons; +} + +export type MWProviderMediaResult = Omit; + +export interface MWQuery { + searchQuery: string; + type: MWMediaType; +} + +export interface MWMediaProviderBase { + id: string; // id of provider, must be unique + enabled: boolean; + type: MWMediaType[]; + displayName: string; + + getMediaFromPortable(media: MWPortableMedia): Promise; + searchForMedia(query: MWQuery): Promise; + getStream(media: MWPortableMedia): Promise; + getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise; +} + +export type MWMediaProviderSeries = MWMediaProviderBase & { + getSeasonDataFromMedia: (media: MWPortableMedia) => Promise; +}; + +export type MWMediaProvider = MWMediaProviderBase; + +export interface MWMediaProviderMetadata { + exists: boolean; + id?: string; + enabled: boolean; + type: MWMediaType[]; + provider?: MWMediaProvider; +} + +export interface MWMassProviderOutput { + providers: { + id: string; + success: boolean; + }[]; + results: MWMedia[]; + stats: { + total: number; + failed: number; + succeeded: number; + }; +} diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 8ab960b1..312380b1 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -1,8 +1,8 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; -import Backend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; i18n // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) @@ -17,12 +17,11 @@ i18n // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ - fallbackLng: 'en-GB', + fallbackLng: "en-GB", interpolation: { escapeValue: false, // not needed for react as it escapes by default - } + }, }); - -export default i18n; \ No newline at end of file +export default i18n; diff --git a/src/state/bookmark/index.ts b/src/state/bookmark/index.ts index 1b3fa9eb..2edd280c 100644 --- a/src/state/bookmark/index.ts +++ b/src/state/bookmark/index.ts @@ -1 +1 @@ -export * from "./context"; \ No newline at end of file +export * from "./context"; diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 02ea86e0..88fe66df 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,5 +1,6 @@ import { ReactElement, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; @@ -29,7 +30,6 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; -import { useTranslation } from "react-i18next"; import { NotFoundChecks } from "./notfound/NotFoundChecks"; interface StyledMediaViewProps { diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index 6ba492ac..14cb7829 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -1,10 +1,10 @@ import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; 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 { useTranslation } from "react-i18next"; function NotFoundWrapper(props: { children?: ReactNode }) { return ( diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx index 180d456e..16f68e7c 100644 --- a/src/views/search/HomeView.tsx +++ b/src/views/search/HomeView.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; @@ -7,7 +8,6 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { useWatchedContext } from "@/state/watched"; -import { useTranslation } from "react-i18next"; function Bookmarks() { const { t } = useTranslation(); diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 6d02ff51..54cbeef9 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -1,5 +1,5 @@ -import { Loading } from "@/components/layout/Loading"; import { useTranslation } from "react-i18next"; +import { Loading } from "@/components/layout/Loading"; export function SearchLoadingView() { const { t } = useTranslation(); diff --git a/src/views/search/SearchResultsPartial.tsx b/src/views/search/SearchResultsPartial.tsx index 96934fa8..59281093 100644 --- a/src/views/search/SearchResultsPartial.tsx +++ b/src/views/search/SearchResultsPartial.tsx @@ -1,6 +1,6 @@ +import { useEffect, useMemo, useState } from "react"; import { useDebounce } from "@/hooks/useDebounce"; import { MWQuery } from "@/providers"; -import { useEffect, useMemo, useState } from "react"; import { HomeView } from "./HomeView"; import { SearchLoadingView } from "./SearchLoadingView"; import { SearchResultsView } from "./SearchResultsView"; diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 5f602988..6a775a23 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; @@ -5,8 +7,6 @@ import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { useLoading } from "@/hooks/useLoading"; import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; import { SearchLoadingView } from "./SearchLoadingView"; function SearchSuffix(props: { diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index c786cbe1..ad61b696 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -1,12 +1,12 @@ import { useCallback, useState } from "react"; +import Sticky from "react-stickynode"; +import { useTranslation } from "react-i18next"; import { Navigation } from "@/components/layout/Navigation"; import { ThinContainer } from "@/components/layout/ThinContainer"; import { SearchBarInput } from "@/components/SearchBar"; -import Sticky from "react-stickynode"; import { Title } from "@/components/text/Title"; import { useSearchQuery } from "@/hooks/useSearchQuery"; import { WideContainer } from "@/components/layout/WideContainer"; -import { useTranslation } from "react-i18next"; import { SearchResultsPartial } from "./SearchResultsPartial"; export function SearchView() {