mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 22:56:06 +00:00
update linting
This commit is contained in:
parent
196d6ae6e5
commit
f93b9b5b0f
|
@ -23,6 +23,11 @@ module.exports = {
|
|||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: "./",
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
},
|
||||
},
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
rules: {
|
||||
"react/jsx-uses-react": "off",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -57,5 +57,5 @@ export function Dropdown(props: DropdownProps) {
|
|||
)}
|
||||
</Listbox>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -8,10 +8,10 @@ export function Loading(props: LoadingProps) {
|
|||
<div className={props.className}>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex h-12 items-center justify-center">
|
||||
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full" />
|
||||
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full [animation-delay:150ms]" />
|
||||
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full [animation-delay:300ms]" />
|
||||
<div className="animate-loading-pin bg-denim-300 mx-1 h-2 w-2 rounded-full [animation-delay:450ms]" />
|
||||
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300" />
|
||||
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300 [animation-delay:150ms]" />
|
||||
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300 [animation-delay:300ms]" />
|
||||
<div className="mx-1 h-2 w-2 animate-loading-pin rounded-full bg-denim-300 [animation-delay:450ms]" />
|
||||
</div>
|
||||
{props.text && props.text.length ? (
|
||||
<p className="mt-3 max-w-xs text-sm opacity-75">{props.text}</p>
|
||||
|
|
|
@ -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 (
|
||||
<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}`}>
|
||||
<div
|
||||
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}`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }) {
|
|||
) : (
|
||||
<div className="flex items-center space-x-3">
|
||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
||||
<p>{t('seasons.failed')}</p>
|
||||
<p>{t("seasons.failed")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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);
|
||||
|
|
|
@ -9,12 +9,12 @@ export function Episode(props: EpisodeProps) {
|
|||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
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 ${
|
||||
props.active ? "shadow-bink-500 shadow-[inset_0_0_0_2px]" : ""
|
||||
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 ${
|
||||
props.active ? "shadow-[inset_0_0_0_2px] shadow-bink-500" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="bg-bink-500 absolute bottom-0 top-0 left-0 bg-opacity-50"
|
||||
className="absolute bottom-0 top-0 left-0 bg-bink-500 bg-opacity-50"
|
||||
style={{
|
||||
width: `${props.progress || 0}%`,
|
||||
}}
|
||||
|
|
|
@ -5,7 +5,7 @@ export interface DotListProps {
|
|||
|
||||
export function DotList(props: DotListProps) {
|
||||
return (
|
||||
<p className={`text-denim-700 font-semibold ${props.className || ""}`}>
|
||||
<p className={`font-semibold text-denim-700 ${props.className || ""}`}>
|
||||
{props.content.map((item, index) => (
|
||||
<span key={item}>
|
||||
{index !== 0 ? (
|
||||
|
|
|
@ -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 = (
|
||||
<span className="text-bink-600 hover:text-bink-700 cursor-pointer font-bold">
|
||||
<span className="cursor-pointer font-bold text-bink-600 hover:text-bink-700">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (isExternal)
|
||||
return <a target={(props as ILinkPropsExternal).newTab ? "_blank" : undefined} rel="noreferrer" href={(props as ILinkPropsExternal).url}>{content}</a>;
|
||||
return (
|
||||
<a
|
||||
target={(props as ILinkPropsExternal).newTab ? "_blank" : undefined}
|
||||
rel="noreferrer"
|
||||
href={(props as ILinkPropsExternal).url}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
if (isInternal)
|
||||
return (
|
||||
<LinkRouter to={(props as ILinkPropsInternal).to}>{content}</LinkRouter>
|
||||
|
|
|
@ -4,17 +4,14 @@ export function useDebounce<T>(value: T, delay: number): T {
|
|||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(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;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import './useFade.css'
|
||||
import "./useFade.css";
|
||||
|
||||
export const useFade = (initial = false): [boolean, React.Dispatch<React.SetStateAction<boolean>>, any] => {
|
||||
export const useFade = (
|
||||
initial = false
|
||||
): [boolean, React.Dispatch<React.SetStateAction<boolean>>, any] => {
|
||||
const [show, setShow] = useState<boolean>(initial);
|
||||
const [isVisible, setVisible] = useState<boolean>(show);
|
||||
|
||||
|
@ -20,7 +22,7 @@ export const useFade = (initial = false): [boolean, React.Dispatch<React.SetStat
|
|||
// These props go on the fading DOM element
|
||||
const fadeProps = {
|
||||
style,
|
||||
onAnimationEnd
|
||||
onAnimationEnd,
|
||||
};
|
||||
|
||||
return [isVisible, setShow, fadeProps];
|
||||
|
|
|
@ -1,97 +1,97 @@
|
|||
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<MWMedia, "mediaType" | "providerId">;
|
||||
|
||||
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<MWProviderMediaResult>;
|
||||
searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]>;
|
||||
getStream(media: MWPortableMedia): Promise<MWMediaStream>;
|
||||
getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
|
||||
}
|
||||
|
||||
export type MWMediaProviderSeries = MWMediaProviderBase & {
|
||||
getSeasonDataFromMedia: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
|
||||
};
|
||||
|
||||
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<MWMedia, "mediaType" | "providerId">;
|
||||
|
||||
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<MWProviderMediaResult>;
|
||||
searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]>;
|
||||
getStream(media: MWPortableMedia): Promise<MWMediaStream>;
|
||||
getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
|
||||
}
|
||||
|
||||
export type MWMediaProviderSeries = MWMediaProviderBase & {
|
||||
getSeasonDataFromMedia: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
export default i18n;
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from "./context";
|
||||
export * from "./context";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue