bazarr/frontend/src/components/modals/ManualSearchModal.tsx

348 lines
8.4 KiB
TypeScript

import {
faCaretDown,
faCheck,
faDownload,
faInfoCircle,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, {
FunctionComponent,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import {
Badge,
Button,
Col,
Collapse,
Container,
OverlayTrigger,
Popover,
Row,
} from "react-bootstrap";
import { Column } from "react-table";
import {
BaseModal,
BaseModalProps,
LanguageText,
LoadingIndicator,
PageTable,
useModalPayload,
} from "..";
import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilities";
import { ProvidersApi } from "../../apis";
import { GetItemId, isMovie } from "../../utilities";
import "./msmStyle.scss";
type SupportType = Item.Movie | Item.Episode;
enum SearchState {
Ready,
Searching,
Finished,
}
interface Props<T extends SupportType> {
download: (item: T, result: SearchResultType) => Promise<void>;
}
export function ManualSearchModal<T extends SupportType>(
props: Props<T> & BaseModalProps
) {
const { download, ...modal } = props;
const [result, setResult] = useState<SearchResultType[]>([]);
const [searchState, setSearchState] = useState(SearchState.Ready);
const item = useModalPayload<T>(modal.modalKey);
const search = useCallback(async () => {
if (item) {
setSearchState(SearchState.Searching);
let results: SearchResultType[] = [];
if (isMovie(item)) {
results = await ProvidersApi.movies(item.radarrId);
} else {
results = await ProvidersApi.episodes(item.sonarrEpisodeId);
}
setResult(results);
setSearchState(SearchState.Finished);
}
}, [item]);
useEffect(() => {
if (item !== null) {
setSearchState(SearchState.Ready);
}
}, [item]);
const columns = useMemo<Column<SearchResultType>[]>(
() => [
{
Header: "Score",
accessor: (d) => `${d.score}%`,
},
{
accessor: "language",
Cell: ({ row: { original }, value }) => {
const lang: Language.Info = {
code2: value,
hi: original.hearing_impaired === "True",
forced: original.forced === "True",
name: "",
};
return (
<Badge variant="secondary">
<LanguageText text={lang}></LanguageText>
</Badge>
);
},
},
{
Header: "Provider",
accessor: "provider",
Cell: (row) => {
const value = row.value;
const { url } = row.row.original;
if (url) {
return (
<a href={url} target="_blank" rel="noopener noreferrer">
{value}
</a>
);
} else {
return value;
}
},
},
{
Header: "Release",
accessor: "release_info",
className: "text-nowrap",
Cell: (row) => {
const value = row.value;
const [open, setOpen] = useState(false);
const items = useMemo(
() =>
value.slice(1).map((v, idx) => (
<span className="release-text hidden-item" key={idx}>
{v}
</span>
)),
[value]
);
if (value.length === 0) {
return <span className="text-muted">Cannot get release info</span>;
}
const cls = [
"release-container",
"d-flex",
"justify-content-between",
"align-items-center",
];
if (value.length > 1) {
cls.push("release-multi");
}
return (
<div className={cls.join(" ")} onClick={() => setOpen((o) => !o)}>
<div className="text-container">
<span className="release-text">{value[0]}</span>
<Collapse in={open}>
<div>{items}</div>
</Collapse>
</div>
{value.length > 1 && (
<FontAwesomeIcon
className="release-icon"
icon={faCaretDown}
rotation={open ? 180 : undefined}
></FontAwesomeIcon>
)}
</div>
);
},
},
{
Header: "Upload",
accessor: (d) => d.uploader ?? "-",
},
{
accessor: "matches",
Cell: (row) => {
const { matches, dont_matches } = row.row.original;
return <StateIcon matches={matches} dont={dont_matches}></StateIcon>;
},
},
{
accessor: "subtitle",
Cell: ({ row }) => {
const result = row.original;
return (
<Button
size="sm"
variant="light"
disabled={item === null}
onClick={() => {
if (!item) return;
const id = GetItemId(item);
const task = createTask(item.title, id, download, item, result);
dispatchTask(
"Downloading subtitles...",
[task],
"Downloading..."
);
}}
>
<FontAwesomeIcon icon={faDownload}></FontAwesomeIcon>
</Button>
);
},
},
],
[download, item]
);
const content = useMemo<JSX.Element>(() => {
if (searchState === SearchState.Ready) {
return (
<div className="px-4 py-5">
<p className="mb-3 small">{item?.path ?? ""}</p>
<Button variant="primary" block onClick={search}>
Start Search
</Button>
</div>
);
} else if (searchState === SearchState.Searching) {
return <LoadingIndicator animation="grow"></LoadingIndicator>;
} else {
return (
<React.Fragment>
<p className="mb-3 small">{item?.path ?? ""}</p>
<PageTable
emptyText="No Result"
columns={columns}
data={result}
></PageTable>
</React.Fragment>
);
}
}, [searchState, columns, result, search, item?.path]);
const footer = useMemo(
() => (
<Button
variant="light"
hidden={searchState !== SearchState.Finished}
onClick={search}
>
Search Again
</Button>
),
[searchState, search]
);
const title = useMemo(() => {
let title = "Unknown";
if (item) {
if (item.sceneName) {
title = item.sceneName;
} else if (isMovie(item)) {
title = item.title;
} else {
title = item.title;
}
}
return `Search - ${title}`;
}, [item]);
return (
<BaseModal
closeable={searchState !== SearchState.Searching}
size="xl"
title={title}
footer={footer}
{...modal}
>
{content}
</BaseModal>
);
}
const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
matches,
dont,
}) => {
let icon = faCheck;
let color = "var(--success)";
if (dont.length > 0) {
icon = faInfoCircle;
color = "var(--warning)";
}
const matchElements = useMemo(
() =>
matches.map((v, idx) => (
<p key={`match-${idx}`} className="text-nowrap m-0">
{v}
</p>
)),
[matches]
);
const dontElements = useMemo(
() =>
dont.map((v, idx) => (
<p key={`dont-${idx}`} className="text-nowrap m-0">
{v}
</p>
)),
[dont]
);
const popover = useMemo(
() => (
<Popover className="w-100" id="manual-search-matches-info">
<Popover.Content>
<Container fluid>
<Row>
<Col xs={6}>
<FontAwesomeIcon
color="var(--success)"
icon={faCheck}
></FontAwesomeIcon>
{matchElements}
</Col>
<Col xs={6}>
<FontAwesomeIcon
color="var(--danger)"
icon={faTimes}
></FontAwesomeIcon>
{dontElements}
</Col>
</Row>
</Container>
</Popover.Content>
</Popover>
),
[matchElements, dontElements]
);
return (
<OverlayTrigger overlay={popover} placement={"left"}>
<FontAwesomeIcon icon={icon} color={color}></FontAwesomeIcon>
</OverlayTrigger>
);
};