tangled
alpha
login
or
join now
dunkirk.sh
/
pstream-ng
1
fork
atom
pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
1
fork
atom
overview
issues
pulls
pipelines
update linting
Jelle van Snik
3 years ago
f93b9b5b
196d6ae6
+165
-155
24 changed files
expand all
collapse all
unified
split
.eslintrc.js
package.json
src
components
Dropdown.tsx
SearchBar.tsx
layout
Backdrop.tsx
BrandPill.tsx
Loading.tsx
Paper.tsx
Seasons.tsx
media
EpisodeButton.tsx
text
DotList.tsx
Link.tsx
hooks
useDebounce.ts
useFade.ts
providers
types.ts
setup
i18n.ts
state
bookmark
index.ts
views
MediaView.tsx
notfound
NotFoundView.tsx
search
HomeView.tsx
SearchLoadingView.tsx
SearchResultsPartial.tsx
SearchResultsView.tsx
SearchView.tsx
+5
.eslintrc.js
···
23
23
project: "./tsconfig.json",
24
24
tsconfigRootDir: "./",
25
25
},
26
26
+
settings: {
27
27
+
"import/resolver": {
28
28
+
typescript: {},
29
29
+
},
30
30
+
},
26
31
plugins: ["@typescript-eslint", "import"],
27
32
rules: {
28
33
"react/jsx-uses-react": "off",
+1
-1
package.json
···
26
26
"build": "vite build",
27
27
"preview": "vite preview",
28
28
"lint": "eslint --ext .tsx,.ts src",
29
29
-
"lint:strict": "eslint --ext .tsx,.ts --max-warnings 0 src",
29
29
+
"lint:fix": "eslint --fix --ext .tsx,.ts src",
30
30
"lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src"
31
31
},
32
32
"browserslist": {
+1
-1
src/components/Dropdown.tsx
···
57
57
)}
58
58
</Listbox>
59
59
</div>
60
60
-
)
60
60
+
);
61
61
}
+1
-1
src/components/SearchBar.tsx
···
1
1
import { useState } from "react";
2
2
-
import { MWMediaType, MWQuery } from "@/providers";
3
2
import { useTranslation } from "react-i18next";
3
3
+
import { MWMediaType, MWQuery } from "@/providers";
4
4
import { DropdownButton } from "./buttons/DropdownButton";
5
5
import { Icon, Icons } from "./Icon";
6
6
import { TextInputControl } from "./text-inputs/TextInputControl";
+1
-1
src/components/layout/Backdrop.tsx
···
1
1
import React, { createRef, useEffect, useState } from "react";
2
2
-
import { useFade } from "@/hooks/useFade";
3
2
import { createPortal } from "react-dom";
3
3
+
import { useFade } from "@/hooks/useFade";
4
4
5
5
interface BackdropProps {
6
6
onClick?: (e: MouseEvent) => void;
+1
-1
src/components/layout/BrandPill.tsx
···
1
1
-
import { Icon, Icons } from "@/components/Icon";
2
1
import { useTranslation } from "react-i18next";
2
2
+
import { Icon, Icons } from "@/components/Icon";
3
3
4
4
export function BrandPill(props: { clickable?: boolean }) {
5
5
const { t } = useTranslation();
+4
-4
src/components/layout/Loading.tsx
···
8
8
<div className={props.className}>
9
9
<div className="flex flex-col items-center justify-center">
10
10
<div className="flex h-12 items-center justify-center">
11
11
-
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full" />
12
12
-
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full [animation-delay:150ms]" />
13
13
-
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full [animation-delay:300ms]" />
14
14
-
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full [animation-delay:450ms]" />
11
11
+
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300" />
12
12
+
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300 [animation-delay:150ms]" />
13
13
+
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300 [animation-delay:300ms]" />
14
14
+
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300 [animation-delay:450ms]" />
15
15
</div>
16
16
{props.text && props.text.length ? (
17
17
<p className="mt-3 max-w-xs text-sm opacity-75">{props.text}</p>
+6
-4
src/components/layout/Paper.tsx
···
1
1
import { ReactNode } from "react";
2
2
3
3
export interface PaperProps {
4
4
-
children?: ReactNode,
5
5
-
className?: string,
4
4
+
children?: ReactNode;
5
5
+
className?: string;
6
6
}
7
7
8
8
export function Paper(props: PaperProps) {
9
9
return (
10
10
-
<div className={`bg-denim-200 lg:rounded-xl px-4 sm:px-8 md:px-12 py-6 sm:py-8 md:py-12 ${props.className}`}>
10
10
+
<div
11
11
+
className={`bg-denim-200 px-4 py-6 sm:px-8 sm:py-8 md:px-12 md:py-12 lg:rounded-xl ${props.className}`}
12
12
+
>
11
13
{props.children}
12
14
</div>
13
13
-
)
15
15
+
);
14
16
}
+3
-3
src/components/layout/Seasons.tsx
···
1
1
import { useEffect, useState } from "react";
2
2
import { useHistory } from "react-router-dom";
3
3
+
import { useTranslation } from "react-i18next";
3
4
import { IconPatch } from "@/components/buttons/IconPatch";
4
5
import { Dropdown, OptionItem } from "@/components/Dropdown";
5
6
import { Icons } from "@/components/Icon";
···
14
15
MWPortableMedia,
15
16
} from "@/providers";
16
17
import { getSeasonDataFromMedia } from "@/providers/methods/seasons";
17
17
-
import { useTranslation } from "react-i18next";
18
18
19
19
export interface SeasonsProps {
20
20
media: MWMedia;
···
37
37
) : (
38
38
<div className="flex items-center space-x-3">
39
39
<IconPatch icon={Icons.WARNING} className="text-red-400" />
40
40
-
<p>{t('seasons.failed')}</p>
40
40
+
<p>{t("seasons.failed")}</p>
41
41
</div>
42
42
)}
43
43
</div>
···
75
75
76
76
const mapSeason = (season: MWMediaSeason) => ({
77
77
id: season.id,
78
78
-
name: season.title || `${t('seasons.season', { season: season.sort })}`,
78
78
+
name: season.title || `${t("seasons.season", { season: season.sort })}`,
79
79
});
80
80
81
81
const options = seasons.seasons.map(mapSeason);
+3
-3
src/components/media/EpisodeButton.tsx
···
9
9
return (
10
10
<div
11
11
onClick={props.onClick}
12
12
-
className={`bg-denim-500 hover:bg-denim-400 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 font-bold text-white active:scale-110 ${
13
13
-
props.active ? "shadow-bink-500 shadow-[inset_0_0_0_2px]" : ""
12
12
+
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 ${
13
13
+
props.active ? "shadow-[inset_0_0_0_2px] shadow-bink-500" : ""
14
14
}`}
15
15
>
16
16
<div
17
17
-
className="bg-bink-500 absolute bottom-0 top-0 left-0 bg-opacity-50"
17
17
+
className="absolute bottom-0 top-0 left-0 bg-bink-500 bg-opacity-50"
18
18
style={{
19
19
width: `${props.progress || 0}%`,
20
20
}}
+1
-1
src/components/text/DotList.tsx
···
5
5
6
6
export function DotList(props: DotListProps) {
7
7
return (
8
8
-
<p className={`text-denim-700 font-semibold ${props.className || ""}`}>
8
8
+
<p className={`font-semibold text-denim-700 ${props.className || ""}`}>
9
9
{props.content.map((item, index) => (
10
10
<span key={item}>
11
11
{index !== 0 ? (
+11
-6
src/components/text/Link.tsx
···
16
16
to: string;
17
17
}
18
18
19
19
-
type LinkProps =
20
20
-
| ILinkPropsExternal
21
21
-
| ILinkPropsInternal
22
22
-
| ILinkPropsBase;
19
19
+
type LinkProps = ILinkPropsExternal | ILinkPropsInternal | ILinkPropsBase;
23
20
24
21
export function Link(props: LinkProps) {
25
22
const isExternal = !!(props as ILinkPropsExternal).url;
26
23
const isInternal = !!(props as ILinkPropsInternal).to;
27
24
const content = (
28
28
-
<span className="text-bink-600 hover:text-bink-700 cursor-pointer font-bold">
25
25
+
<span className="cursor-pointer font-bold text-bink-600 hover:text-bink-700">
29
26
{props.children}
30
27
</span>
31
28
);
32
29
33
30
if (isExternal)
34
34
-
return <a target={(props as ILinkPropsExternal).newTab ? "_blank" : undefined} rel="noreferrer" href={(props as ILinkPropsExternal).url}>{content}</a>;
31
31
+
return (
32
32
+
<a
33
33
+
target={(props as ILinkPropsExternal).newTab ? "_blank" : undefined}
34
34
+
rel="noreferrer"
35
35
+
href={(props as ILinkPropsExternal).url}
36
36
+
>
37
37
+
{content}
38
38
+
</a>
39
39
+
);
35
40
if (isInternal)
36
41
return (
37
42
<LinkRouter to={(props as ILinkPropsInternal).to}>{content}</LinkRouter>
+8
-11
src/hooks/useDebounce.ts
···
4
4
// State and setters for debounced value
5
5
const [debouncedValue, setDebouncedValue] = useState<T>(value);
6
6
7
7
-
useEffect(
8
8
-
() => {
9
9
-
const handler = setTimeout(() => {
10
10
-
setDebouncedValue(value);
11
11
-
}, delay);
12
12
-
return () => {
13
13
-
clearTimeout(handler);
14
14
-
};
15
15
-
},
16
16
-
[value, delay]
17
17
-
);
7
7
+
useEffect(() => {
8
8
+
const handler = setTimeout(() => {
9
9
+
setDebouncedValue(value);
10
10
+
}, delay);
11
11
+
return () => {
12
12
+
clearTimeout(handler);
13
13
+
};
14
14
+
}, [value, delay]);
18
15
19
16
return debouncedValue;
20
17
}
+5
-3
src/hooks/useFade.ts
···
1
1
import React, { useEffect, useState } from "react";
2
2
-
import './useFade.css'
2
2
+
import "./useFade.css";
3
3
4
4
-
export const useFade = (initial = false): [boolean, React.Dispatch<React.SetStateAction<boolean>>, any] => {
4
4
+
export const useFade = (
5
5
+
initial = false
6
6
+
): [boolean, React.Dispatch<React.SetStateAction<boolean>>, any] => {
5
7
const [show, setShow] = useState<boolean>(initial);
6
8
const [isVisible, setVisible] = useState<boolean>(show);
7
9
···
20
22
// These props go on the fading DOM element
21
23
const fadeProps = {
22
24
style,
23
23
-
onAnimationEnd
25
25
+
onAnimationEnd,
24
26
};
25
27
26
28
return [isVisible, setShow, fadeProps];
+97
-97
src/providers/types.ts
···
1
1
-
export enum MWMediaType {
2
2
-
MOVIE = "movie",
3
3
-
SERIES = "series",
4
4
-
ANIME = "anime",
5
5
-
}
6
6
-
7
7
-
export interface MWPortableMedia {
8
8
-
mediaId: string;
9
9
-
mediaType: MWMediaType;
10
10
-
providerId: string;
11
11
-
seasonId?: string;
12
12
-
episodeId?: string;
13
13
-
}
14
14
-
15
15
-
export type MWMediaStreamType = "m3u8" | "mp4";
16
16
-
export interface MWMediaCaption {
17
17
-
id: string;
18
18
-
url: string;
19
19
-
label: string;
20
20
-
}
21
21
-
export interface MWMediaStream {
22
22
-
url: string;
23
23
-
type: MWMediaStreamType;
24
24
-
captions: MWMediaCaption[];
25
25
-
}
26
26
-
27
27
-
export interface MWMediaMeta extends MWPortableMedia {
28
28
-
title: string;
29
29
-
year: string;
30
30
-
seasonCount?: number;
31
31
-
}
32
32
-
33
33
-
export interface MWMediaEpisode {
34
34
-
sort: number;
35
35
-
id: string;
36
36
-
title: string;
37
37
-
}
38
38
-
export interface MWMediaSeason {
39
39
-
sort: number;
40
40
-
id: string;
41
41
-
title?: string;
42
42
-
type: "season" | "special";
43
43
-
episodes: MWMediaEpisode[];
44
44
-
}
45
45
-
export interface MWMediaSeasons {
46
46
-
seasons: MWMediaSeason[];
47
47
-
}
48
48
-
49
49
-
export interface MWMedia extends MWMediaMeta {
50
50
-
seriesData?: MWMediaSeasons;
51
51
-
}
52
52
-
53
53
-
export type MWProviderMediaResult = Omit<MWMedia, "mediaType" | "providerId">;
54
54
-
55
55
-
export interface MWQuery {
56
56
-
searchQuery: string;
57
57
-
type: MWMediaType;
58
58
-
}
59
59
-
60
60
-
export interface MWMediaProviderBase {
61
61
-
id: string; // id of provider, must be unique
62
62
-
enabled: boolean;
63
63
-
type: MWMediaType[];
64
64
-
displayName: string;
65
65
-
66
66
-
getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult>;
67
67
-
searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]>;
68
68
-
getStream(media: MWPortableMedia): Promise<MWMediaStream>;
69
69
-
getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
70
70
-
}
71
71
-
72
72
-
export type MWMediaProviderSeries = MWMediaProviderBase & {
73
73
-
getSeasonDataFromMedia: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
74
74
-
};
75
75
-
76
76
-
export type MWMediaProvider = MWMediaProviderBase;
77
77
-
78
78
-
export interface MWMediaProviderMetadata {
79
79
-
exists: boolean;
80
80
-
id?: string;
81
81
-
enabled: boolean;
82
82
-
type: MWMediaType[];
83
83
-
provider?: MWMediaProvider;
84
84
-
}
85
85
-
86
86
-
export interface MWMassProviderOutput {
87
87
-
providers: {
88
88
-
id: string;
89
89
-
success: boolean;
90
90
-
}[];
91
91
-
results: MWMedia[];
92
92
-
stats: {
93
93
-
total: number;
94
94
-
failed: number;
95
95
-
succeeded: number;
96
96
-
};
97
97
-
}
1
1
+
export enum MWMediaType {
2
2
+
MOVIE = "movie",
3
3
+
SERIES = "series",
4
4
+
ANIME = "anime",
5
5
+
}
6
6
+
7
7
+
export interface MWPortableMedia {
8
8
+
mediaId: string;
9
9
+
mediaType: MWMediaType;
10
10
+
providerId: string;
11
11
+
seasonId?: string;
12
12
+
episodeId?: string;
13
13
+
}
14
14
+
15
15
+
export type MWMediaStreamType = "m3u8" | "mp4";
16
16
+
export interface MWMediaCaption {
17
17
+
id: string;
18
18
+
url: string;
19
19
+
label: string;
20
20
+
}
21
21
+
export interface MWMediaStream {
22
22
+
url: string;
23
23
+
type: MWMediaStreamType;
24
24
+
captions: MWMediaCaption[];
25
25
+
}
26
26
+
27
27
+
export interface MWMediaMeta extends MWPortableMedia {
28
28
+
title: string;
29
29
+
year: string;
30
30
+
seasonCount?: number;
31
31
+
}
32
32
+
33
33
+
export interface MWMediaEpisode {
34
34
+
sort: number;
35
35
+
id: string;
36
36
+
title: string;
37
37
+
}
38
38
+
export interface MWMediaSeason {
39
39
+
sort: number;
40
40
+
id: string;
41
41
+
title?: string;
42
42
+
type: "season" | "special";
43
43
+
episodes: MWMediaEpisode[];
44
44
+
}
45
45
+
export interface MWMediaSeasons {
46
46
+
seasons: MWMediaSeason[];
47
47
+
}
48
48
+
49
49
+
export interface MWMedia extends MWMediaMeta {
50
50
+
seriesData?: MWMediaSeasons;
51
51
+
}
52
52
+
53
53
+
export type MWProviderMediaResult = Omit<MWMedia, "mediaType" | "providerId">;
54
54
+
55
55
+
export interface MWQuery {
56
56
+
searchQuery: string;
57
57
+
type: MWMediaType;
58
58
+
}
59
59
+
60
60
+
export interface MWMediaProviderBase {
61
61
+
id: string; // id of provider, must be unique
62
62
+
enabled: boolean;
63
63
+
type: MWMediaType[];
64
64
+
displayName: string;
65
65
+
66
66
+
getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult>;
67
67
+
searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]>;
68
68
+
getStream(media: MWPortableMedia): Promise<MWMediaStream>;
69
69
+
getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
70
70
+
}
71
71
+
72
72
+
export type MWMediaProviderSeries = MWMediaProviderBase & {
73
73
+
getSeasonDataFromMedia: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
74
74
+
};
75
75
+
76
76
+
export type MWMediaProvider = MWMediaProviderBase;
77
77
+
78
78
+
export interface MWMediaProviderMetadata {
79
79
+
exists: boolean;
80
80
+
id?: string;
81
81
+
enabled: boolean;
82
82
+
type: MWMediaType[];
83
83
+
provider?: MWMediaProvider;
84
84
+
}
85
85
+
86
86
+
export interface MWMassProviderOutput {
87
87
+
providers: {
88
88
+
id: string;
89
89
+
success: boolean;
90
90
+
}[];
91
91
+
results: MWMedia[];
92
92
+
stats: {
93
93
+
total: number;
94
94
+
failed: number;
95
95
+
succeeded: number;
96
96
+
};
97
97
+
}
+7
-8
src/setup/i18n.ts
···
1
1
-
import i18n from 'i18next';
2
2
-
import { initReactI18next } from 'react-i18next';
1
1
+
import i18n from "i18next";
2
2
+
import { initReactI18next } from "react-i18next";
3
3
4
4
-
import Backend from 'i18next-http-backend';
5
5
-
import LanguageDetector from 'i18next-browser-languagedetector';
4
4
+
import Backend from "i18next-http-backend";
5
5
+
import LanguageDetector from "i18next-browser-languagedetector";
6
6
7
7
i18n
8
8
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
···
17
17
// init i18next
18
18
// for all options read: https://www.i18next.com/overview/configuration-options
19
19
.init({
20
20
-
fallbackLng: 'en-GB',
20
20
+
fallbackLng: "en-GB",
21
21
22
22
interpolation: {
23
23
escapeValue: false, // not needed for react as it escapes by default
24
24
-
}
24
24
+
},
25
25
});
26
26
27
27
-
28
28
-
export default i18n;
27
27
+
export default i18n;
+1
-1
src/state/bookmark/index.ts
···
1
1
-
export * from "./context";
1
1
+
export * from "./context";
+1
-1
src/views/MediaView.tsx
···
1
1
import { ReactElement, useEffect, useState } from "react";
2
2
import { useHistory } from "react-router-dom";
3
3
+
import { useTranslation } from "react-i18next";
3
4
import { IconPatch } from "@/components/buttons/IconPatch";
4
5
import { Icons } from "@/components/Icon";
5
6
import { Navigation } from "@/components/layout/Navigation";
···
29
30
useBookmarkContext,
30
31
} from "@/state/bookmark";
31
32
import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
32
32
-
import { useTranslation } from "react-i18next";
33
33
import { NotFoundChecks } from "./notfound/NotFoundChecks";
34
34
35
35
interface StyledMediaViewProps {
+1
-1
src/views/notfound/NotFoundView.tsx
···
1
1
import { ReactNode } from "react";
2
2
+
import { useTranslation } from "react-i18next";
2
3
import { IconPatch } from "@/components/buttons/IconPatch";
3
4
import { Icons } from "@/components/Icon";
4
5
import { Navigation } from "@/components/layout/Navigation";
5
6
import { ArrowLink } from "@/components/text/ArrowLink";
6
7
import { Title } from "@/components/text/Title";
7
7
-
import { useTranslation } from "react-i18next";
8
8
9
9
function NotFoundWrapper(props: { children?: ReactNode }) {
10
10
return (
+1
-1
src/views/search/HomeView.tsx
···
1
1
+
import { useTranslation } from "react-i18next";
1
2
import { Icons } from "@/components/Icon";
2
3
import { SectionHeading } from "@/components/layout/SectionHeading";
3
4
import { MediaGrid } from "@/components/media/MediaGrid";
···
7
8
useBookmarkContext,
8
9
} from "@/state/bookmark";
9
10
import { useWatchedContext } from "@/state/watched";
10
10
-
import { useTranslation } from "react-i18next";
11
11
12
12
function Bookmarks() {
13
13
const { t } = useTranslation();
+1
-1
src/views/search/SearchLoadingView.tsx
···
1
1
-
import { Loading } from "@/components/layout/Loading";
2
1
import { useTranslation } from "react-i18next";
2
2
+
import { Loading } from "@/components/layout/Loading";
3
3
4
4
export function SearchLoadingView() {
5
5
const { t } = useTranslation();
+1
-1
src/views/search/SearchResultsPartial.tsx
···
1
1
+
import { useEffect, useMemo, useState } from "react";
1
2
import { useDebounce } from "@/hooks/useDebounce";
2
3
import { MWQuery } from "@/providers";
3
3
-
import { useEffect, useMemo, useState } from "react";
4
4
import { HomeView } from "./HomeView";
5
5
import { SearchLoadingView } from "./SearchLoadingView";
6
6
import { SearchResultsView } from "./SearchResultsView";
+2
-2
src/views/search/SearchResultsView.tsx
···
1
1
+
import { useEffect, useState } from "react";
2
2
+
import { useTranslation } from "react-i18next";
1
3
import { IconPatch } from "@/components/buttons/IconPatch";
2
4
import { Icons } from "@/components/Icon";
3
5
import { SectionHeading } from "@/components/layout/SectionHeading";
···
5
7
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
6
8
import { useLoading } from "@/hooks/useLoading";
7
9
import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers";
8
8
-
import { useEffect, useState } from "react";
9
9
-
import { useTranslation } from "react-i18next";
10
10
import { SearchLoadingView } from "./SearchLoadingView";
11
11
12
12
function SearchSuffix(props: {
+2
-2
src/views/search/SearchView.tsx
···
1
1
import { useCallback, useState } from "react";
2
2
+
import Sticky from "react-stickynode";
3
3
+
import { useTranslation } from "react-i18next";
2
4
import { Navigation } from "@/components/layout/Navigation";
3
5
import { ThinContainer } from "@/components/layout/ThinContainer";
4
6
import { SearchBarInput } from "@/components/SearchBar";
5
5
-
import Sticky from "react-stickynode";
6
7
import { Title } from "@/components/text/Title";
7
8
import { useSearchQuery } from "@/hooks/useSearchQuery";
8
9
import { WideContainer } from "@/components/layout/WideContainer";
9
9
-
import { useTranslation } from "react-i18next";
10
10
import { SearchResultsPartial } from "./SearchResultsPartial";
11
11
12
12
export function SearchView() {