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

277 lines
7.3 KiB
TypeScript
Raw Normal View History

2022-05-31 15:49:04 +00:00
import { withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { BuildKey, GetItemId } from "@/utilities";
2021-03-25 14:22:43 +00:00
import {
faCaretDown,
2021-03-25 14:22:43 +00:00
faCheck,
2022-05-31 15:49:04 +00:00
faCheckCircle,
2021-03-25 14:22:43 +00:00
faDownload,
2022-05-31 15:49:04 +00:00
faExclamationCircle,
2021-03-25 14:22:43 +00:00
faInfoCircle,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
2022-05-31 15:49:04 +00:00
Alert,
Anchor,
2021-03-25 14:22:43 +00:00
Badge,
Button,
Code,
Collapse,
2022-05-31 15:49:04 +00:00
Divider,
Group,
List,
2021-03-25 14:22:43 +00:00
Popover,
2022-05-31 15:49:04 +00:00
Stack,
Text,
} from "@mantine/core";
import { useHover } from "@mantine/hooks";
import { isString } from "lodash";
2022-05-31 15:49:04 +00:00
import { FunctionComponent, useCallback, useMemo, useState } from "react";
2022-03-20 04:57:44 +00:00
import { UseQueryResult } from "react-query";
2021-03-25 14:22:43 +00:00
import { Column } from "react-table";
2022-05-31 15:49:04 +00:00
import { Action, PageTable } from "..";
import Language from "../bazarr/Language";
2021-03-25 14:22:43 +00:00
type SupportType = Item.Movie | Item.Episode;
interface Props<T extends SupportType> {
download: (item: T, result: SearchResultType) => Promise<void>;
2022-03-20 04:57:44 +00:00
query: (
id?: number
) => UseQueryResult<SearchResultType[] | undefined, unknown>;
2022-05-31 15:49:04 +00:00
item: T;
2021-03-25 14:22:43 +00:00
}
2022-03-27 06:42:28 +00:00
function ManualSearchView<T extends SupportType>(props: Props<T>) {
2022-05-31 15:49:04 +00:00
const { download, query: useSearch, item } = props;
2021-03-25 14:22:43 +00:00
2022-03-20 04:57:44 +00:00
const itemId = useMemo(() => GetItemId(item ?? {}), [item]);
2022-03-20 04:57:44 +00:00
const [id, setId] = useState<number | undefined>(undefined);
2022-03-20 04:57:44 +00:00
const results = useSearch(id);
const isStale = results.data === undefined;
const search = useCallback(() => {
2022-05-31 15:49:04 +00:00
setId(itemId);
results.refetch();
2022-03-20 04:57:44 +00:00
}, [itemId, results]);
2021-03-25 14:22:43 +00:00
const columns = useMemo<Column<SearchResultType>[]>(
() => [
{
Header: "Score",
2022-05-31 15:49:04 +00:00
accessor: "score",
Cell: ({ value }) => {
const { classes } = useTableStyles();
return <Text className={classes.noWrap}>{value}%</Text>;
},
2021-03-25 14:22:43 +00:00
},
{
accessor: "language",
Cell: ({ row: { original }, value }) => {
const lang: Language.Info = {
2021-03-25 14:22:43 +00:00
code2: value,
hi: original.hearing_impaired === "True",
forced: original.forced === "True",
name: "",
};
return (
2022-05-31 15:49:04 +00:00
<Badge>
<Language.Text value={lang}></Language.Text>
2021-03-25 14:22:43 +00:00
</Badge>
);
},
},
{
Header: "Provider",
accessor: "provider",
Cell: (row) => {
2022-05-31 15:49:04 +00:00
const { classes } = useTableStyles();
2021-03-25 14:22:43 +00:00
const value = row.value;
const { url } = row.row.original;
if (url) {
return (
2022-05-31 15:49:04 +00:00
<Anchor
className={classes.noWrap}
href={url}
target="_blank"
rel="noopener noreferrer"
>
2021-03-25 14:22:43 +00:00
{value}
2022-05-31 15:49:04 +00:00
</Anchor>
2021-03-25 14:22:43 +00:00
);
} else {
return value;
}
},
},
{
Header: "Release",
accessor: "release_info",
2022-05-31 15:49:04 +00:00
Cell: ({ value }) => {
const { classes } = useTableStyles();
const [open, setOpen] = useState(false);
2021-03-25 14:22:43 +00:00
const items = useMemo(
2022-05-31 15:49:04 +00:00
() => value.slice(1).map((v, idx) => <Text key={idx}>{v}</Text>),
2021-03-25 14:22:43 +00:00
[value]
);
if (value.length === 0) {
2022-05-31 15:49:04 +00:00
return <Text color="dimmed">Cannot get release info</Text>;
2021-03-25 14:22:43 +00:00
}
return (
2022-05-31 15:49:04 +00:00
<Stack spacing={0} onClick={() => setOpen((o) => !o)}>
<Text className={classes.primary}>
{value[0]}
{value.length > 1 && (
<FontAwesomeIcon
icon={faCaretDown}
rotation={open ? 180 : undefined}
></FontAwesomeIcon>
)}
</Text>
<Collapse in={open}>
<>{items}</>
</Collapse>
</Stack>
);
2021-03-25 14:22:43 +00:00
},
},
{
Header: "Upload",
2022-05-31 15:49:04 +00:00
accessor: "uploader",
Cell: ({ value }) => {
const { classes } = useTableStyles();
return <Text className={classes.noWrap}>{value ?? "-"}</Text>;
},
2021-03-25 14:22:43 +00:00
},
{
accessor: "matches",
Cell: (row) => {
const { matches, dont_matches } = row.row.original;
return <StateIcon matches={matches} dont={dont_matches}></StateIcon>;
},
},
2021-03-25 18:24:22 +00:00
{
accessor: "subtitle",
Cell: ({ row }) => {
2021-03-25 18:24:22 +00:00
const result = row.original;
return (
2022-05-31 15:49:04 +00:00
<Action
2022-06-01 17:08:50 +00:00
label="Download"
2022-05-31 15:49:04 +00:00
icon={faDownload}
color="brand"
2021-03-25 18:24:22 +00:00
variant="light"
disabled={item === null}
onClick={() => {
if (!item) return;
2022-05-31 15:49:04 +00:00
task.create(
item.title,
2022-05-31 15:49:04 +00:00
TaskGroup.DownloadSubtitle,
download,
item,
result
);
}}
2022-05-31 15:49:04 +00:00
></Action>
2021-03-25 18:24:22 +00:00
);
},
},
2021-03-25 14:22:43 +00:00
],
[download, item]
2021-03-25 14:22:43 +00:00
);
const bSceneNameAvailable =
isString(item.sceneName) && item.sceneName.length !== 0;
2021-03-25 14:22:43 +00:00
return (
2022-05-31 15:49:04 +00:00
<Stack>
<Alert
title="Resource"
color="gray"
icon={<FontAwesomeIcon icon={faInfoCircle}></FontAwesomeIcon>}
>
<Text size="sm">{item?.path}</Text>
<Divider hidden={!bSceneNameAvailable} my="xs"></Divider>
<Code hidden={!bSceneNameAvailable}>{item?.sceneName}</Code>
2022-05-31 15:49:04 +00:00
</Alert>
<Collapse in={!isStale && !results.isFetching}>
<PageTable
tableStyles={{ emptyText: "No result", placeholder: 10 }}
columns={columns}
data={results.data ?? []}
></PageTable>
</Collapse>
<Divider></Divider>
<Button loading={results.isFetching} fullWidth onClick={search}>
{isStale ? "Search" : "Search Again"}
</Button>
</Stack>
2021-03-25 14:22:43 +00:00
);
}
2021-03-25 14:22:43 +00:00
2022-03-27 06:42:28 +00:00
export const MovieSearchModal = withModal<Props<Item.Movie>>(
ManualSearchView,
2022-05-31 15:49:04 +00:00
"movie-manual-search",
{ title: "Search Subtitles", size: "xl" }
2022-03-27 06:42:28 +00:00
);
export const EpisodeSearchModal = withModal<Props<Item.Episode>>(
ManualSearchView,
2022-05-31 15:49:04 +00:00
"episode-manual-search",
{ title: "Search Subtitles", size: "xl" }
2022-03-27 06:42:28 +00:00
);
2021-03-25 14:22:43 +00:00
const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
matches,
dont,
}) => {
2022-05-31 15:49:04 +00:00
const hasIssues = dont.length > 0;
2021-03-25 14:22:43 +00:00
2022-05-31 15:49:04 +00:00
const { ref, hovered } = useHover();
2021-03-25 14:22:43 +00:00
return (
2022-05-31 15:49:04 +00:00
<Popover
opened={hovered}
placement="center"
position="top"
target={
<Text color={hasIssues ? "yellow" : "green"} ref={ref}>
<FontAwesomeIcon
icon={hasIssues ? faExclamationCircle : faCheckCircle}
></FontAwesomeIcon>
</Text>
}
>
<Group align="flex-start" spacing="xl">
<Stack align="flex-start" spacing="xs">
<Text color="green">
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
</Text>
<List>
{matches.map((v, idx) => (
<List.Item key={BuildKey(idx, v, "match")}>{v}</List.Item>
))}
</List>
</Stack>
<Stack align="flex-start" spacing="xs">
<Text color="yellow">
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
</Text>
<List>
{dont.map((v, idx) => (
<List.Item key={BuildKey(idx, v, "miss")}>{v}</List.Item>
))}
</List>
</Stack>
</Group>
</Popover>
2021-03-25 14:22:43 +00:00
);
};