mirror of
https://github.com/morpheus65535/bazarr
synced 2025-02-20 21:17:02 +00:00
Upgrade Mantine to Version 5 (#1930)
* Upgrade Mantine to v5.2.3 * Remove react-dropzone and replace with latest @mantine/dropzone * Fix issues
This commit is contained in:
parent
978b378683
commit
a0f8d75f04
41 changed files with 2589 additions and 1521 deletions
3087
frontend/package-lock.json
generated
3087
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,8 +13,12 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@mantine/core": "^4",
|
||||
"@mantine/hooks": "^4",
|
||||
"@mantine/core": "^5",
|
||||
"@mantine/form": "^5",
|
||||
"@mantine/hooks": "^5",
|
||||
"@mantine/modals": "^5",
|
||||
"@mantine/notifications": "^5",
|
||||
"@mantine/dropzone": "^5",
|
||||
"axios": "^0.27",
|
||||
"react": "^17",
|
||||
"react-dom": "^17",
|
||||
|
@ -29,8 +33,6 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6",
|
||||
"@fortawesome/free-solid-svg-icons": "^6",
|
||||
"@fortawesome/react-fontawesome": "^0.1",
|
||||
"@mantine/modals": "^4",
|
||||
"@mantine/notifications": "^4",
|
||||
"@testing-library/jest-dom": "latest",
|
||||
"@testing-library/react": "12",
|
||||
"@testing-library/react-hooks": "latest",
|
||||
|
@ -52,7 +54,6 @@
|
|||
"prettier": "^2",
|
||||
"prettier-plugin-organize-imports": "^3",
|
||||
"pretty-quick": "^3",
|
||||
"react-dropzone": "^14",
|
||||
"react-table": "^7",
|
||||
"recharts": "^2",
|
||||
"sass": "^1",
|
||||
|
|
|
@ -81,8 +81,8 @@ const AppHeader: FunctionComponent = () => {
|
|||
</Group>
|
||||
<Group spacing="xs" position="right" noWrap>
|
||||
<Search></Search>
|
||||
<Menu
|
||||
control={
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Action
|
||||
label="System"
|
||||
tooltip={{ position: "left", openDelay: 2000 }}
|
||||
|
@ -92,24 +92,25 @@ const AppHeader: FunctionComponent = () => {
|
|||
size="lg"
|
||||
variant="light"
|
||||
></Action>
|
||||
}
|
||||
>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
||||
onClick={() => restart()}
|
||||
>
|
||||
Restart
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faPowerOff} />}
|
||||
onClick={() => shutdown()}
|
||||
>
|
||||
Shutdown
|
||||
</Menu.Item>
|
||||
<Divider hidden={!hasLogout}></Divider>
|
||||
<Menu.Item hidden={!hasLogout} onClick={() => logout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
||||
onClick={() => restart()}
|
||||
>
|
||||
Restart
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faPowerOff} />}
|
||||
onClick={() => shutdown()}
|
||||
>
|
||||
Shutdown
|
||||
</Menu.Item>
|
||||
<Divider hidden={!hasLogout}></Divider>
|
||||
<Menu.Item hidden={!hasLogout} onClick={() => logout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
@ -138,7 +138,7 @@ const AppNavbar: FunctionComponent = () => {
|
|||
<Action
|
||||
label="Change Theme"
|
||||
color={dark ? "yellow" : "indigo"}
|
||||
variant="hover"
|
||||
variant="subtle"
|
||||
onClick={() => toggleColorScheme()}
|
||||
icon={dark ? faSun : faMoon}
|
||||
></Action>
|
||||
|
@ -149,7 +149,7 @@ const AppNavbar: FunctionComponent = () => {
|
|||
<Action
|
||||
label="Donate"
|
||||
icon={faHeart}
|
||||
variant="hover"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
></Action>
|
||||
</Anchor>
|
||||
|
@ -254,8 +254,16 @@ const useStyles = createStyles((theme) => {
|
|||
const hoverBackgroundColor =
|
||||
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[7];
|
||||
|
||||
const textColor =
|
||||
theme.colorScheme === "light" ? theme.colors.gray[8] : theme.colors.gray[5];
|
||||
|
||||
return {
|
||||
text: { display: "inline-flex", alignItems: "center", width: "100%" },
|
||||
text: {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
color: textColor,
|
||||
},
|
||||
anchor: {
|
||||
textDecoration: "none",
|
||||
borderLeft: `2px solid ${borderColor}`,
|
||||
|
@ -273,6 +281,7 @@ const useStyles = createStyles((theme) => {
|
|||
marginLeft: "auto",
|
||||
textDecoration: "none",
|
||||
boxShadow: theme.shadows.xs,
|
||||
color: textColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -323,7 +332,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
|||
inline
|
||||
p="xs"
|
||||
size="sm"
|
||||
color="gray"
|
||||
weight={primary ? "bold" : "normal"}
|
||||
className={classes.text}
|
||||
>
|
||||
|
@ -336,7 +344,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
|||
{name}
|
||||
<Badge
|
||||
className={classes.badge}
|
||||
color="gray"
|
||||
radius="xs"
|
||||
hidden={badge === undefined || badge === 0}
|
||||
>
|
||||
|
|
|
@ -5,6 +5,7 @@ import NavbarProvider from "@/contexts/Navbar";
|
|||
import OnlineProvider from "@/contexts/Online";
|
||||
import { notification } from "@/modules/task";
|
||||
import CriticalError from "@/pages/CriticalError";
|
||||
import { RouterNames } from "@/Router/RouterNames";
|
||||
import { Environment } from "@/utilities";
|
||||
import { AppShell } from "@mantine/core";
|
||||
import { useWindowEvent } from "@mantine/hooks";
|
||||
|
@ -26,7 +27,7 @@ const App: FunctionComponent = () => {
|
|||
|
||||
useWindowEvent("app-auth-changed", (ev) => {
|
||||
if (!ev.detail.authenticated) {
|
||||
navigate("/login");
|
||||
navigate(RouterNames.Auth);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
ColorScheme,
|
||||
ColorSchemeProvider,
|
||||
createEmotionCache,
|
||||
MantineProvider,
|
||||
MantineThemeOverride,
|
||||
} from "@mantine/core";
|
||||
|
@ -49,6 +50,8 @@ function useAutoColorScheme() {
|
|||
return { colorScheme, setColorScheme, toggleColorScheme };
|
||||
}
|
||||
|
||||
const emotionCache = createEmotionCache({ key: "bazarr" });
|
||||
|
||||
const ThemeProvider: FunctionComponent = ({ children }) => {
|
||||
const { colorScheme, toggleColorScheme } = useAutoColorScheme();
|
||||
|
||||
|
@ -61,7 +64,7 @@ const ThemeProvider: FunctionComponent = ({ children }) => {
|
|||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{ colorScheme, ...theme }}
|
||||
emotionOptions={{ key: "bazarr" }}
|
||||
emotionCache={emotionCache}
|
||||
>
|
||||
{children}
|
||||
</MantineProvider>
|
||||
|
|
4
frontend/src/Router/RouterNames.ts
Normal file
4
frontend/src/Router/RouterNames.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const RouterNames = {
|
||||
Auth: "/login",
|
||||
NotFound: "/not-found",
|
||||
};
|
|
@ -49,6 +49,7 @@ import {
|
|||
} from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import Redirector from "./Redirector";
|
||||
import { RouterNames } from "./RouterNames";
|
||||
import { CustomRouteObject } from "./type";
|
||||
|
||||
const HistoryStats = lazy(() => import("@/pages/History/Statistics"));
|
||||
|
@ -285,7 +286,7 @@ function useRoutes(): CustomRouteObject[] {
|
|||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
path: RouterNames.Auth,
|
||||
hidden: true,
|
||||
element: <Authentication></Authentication>,
|
||||
},
|
||||
|
|
|
@ -105,7 +105,7 @@ export function useTools() {
|
|||
interface Props {
|
||||
selections: FormType.ModifySubtitle[];
|
||||
children?: ReactElement;
|
||||
menu?: Omit<MenuProps, "control" | "children">;
|
||||
menu?: Omit<MenuProps, "children">;
|
||||
onAction?: (action: "delete" | "search") => void;
|
||||
}
|
||||
|
||||
|
@ -138,70 +138,67 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
|
|||
const disabledTools = selections.length === 0;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
control={children}
|
||||
withArrow
|
||||
placement="end"
|
||||
position="left"
|
||||
{...menu}
|
||||
>
|
||||
<Menu.Label>Tools</Menu.Label>
|
||||
{tools.map((tool) => (
|
||||
<Menu withArrow position="left-end" {...menu}>
|
||||
<Menu.Target>{children}</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Tools</Menu.Label>
|
||||
{tools.map((tool) => (
|
||||
<Menu.Item
|
||||
key={tool.key}
|
||||
disabled={disabledTools}
|
||||
icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
if (tool.modal) {
|
||||
modals.openContextModal(tool.modal, { selections });
|
||||
} else {
|
||||
process(tool.key, tool.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tool.name}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Divider></Divider>
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item
|
||||
key={tool.key}
|
||||
disabled={disabledTools}
|
||||
icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
|
||||
disabled={selections.length !== 0 || onAction === undefined}
|
||||
icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
if (tool.modal) {
|
||||
modals.openContextModal(tool.modal, { selections });
|
||||
} else {
|
||||
process(tool.key, tool.name);
|
||||
}
|
||||
onAction?.("search");
|
||||
}}
|
||||
>
|
||||
{tool.name}
|
||||
Search
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Divider></Divider>
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item
|
||||
disabled={selections.length !== 0 || onAction === undefined}
|
||||
icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
onAction?.("search");
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={selections.length === 0 || onAction === undefined}
|
||||
color="red"
|
||||
icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: "The following subtitles will be deleted",
|
||||
size: "lg",
|
||||
children: (
|
||||
<ScrollArea style={{ maxHeight: "20rem" }}>
|
||||
<List>
|
||||
{selections.map((s) => (
|
||||
<List.Item my="md" key={s.path}>
|
||||
{s.path}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</ScrollArea>
|
||||
),
|
||||
onConfirm: () => {
|
||||
onAction?.("delete");
|
||||
},
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete...
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={selections.length === 0 || onAction === undefined}
|
||||
color="red"
|
||||
icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: "The following subtitles will be deleted",
|
||||
size: "lg",
|
||||
children: (
|
||||
<ScrollArea style={{ maxHeight: "20rem" }}>
|
||||
<List>
|
||||
{selections.map((s) => (
|
||||
<List.Item my="md" key={s.path}>
|
||||
{s.path}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</ScrollArea>
|
||||
),
|
||||
onConfirm: () => {
|
||||
onAction?.("delete");
|
||||
},
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete...
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useCallback, useState } from "react";
|
|||
import { UseMutationResult } from "react-query";
|
||||
|
||||
type MutateButtonProps<DATA, VAR> = Omit<
|
||||
ButtonProps<"button">,
|
||||
ButtonProps,
|
||||
"onClick" | "loading" | "color"
|
||||
> & {
|
||||
mutation: UseMutationResult<DATA, unknown, VAR>;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { FunctionComponent } from "react";
|
|||
|
||||
export type AudioListProps = GroupProps & {
|
||||
audios: Language.Info[];
|
||||
badgeProps?: BadgeProps<"div">;
|
||||
badgeProps?: BadgeProps;
|
||||
};
|
||||
|
||||
const AudioList: FunctionComponent<AudioListProps> = ({
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BuildKey } from "@/utilities";
|
|||
import { Badge, Group, Text, TextProps } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
|
||||
type LanguageTextProps = TextProps<"div"> & {
|
||||
type LanguageTextProps = TextProps & {
|
||||
value: Language.Info;
|
||||
long?: boolean;
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Selector, SelectorOption } from "@/components";
|
|||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { task } from "@/modules/task";
|
||||
import { Button, Divider, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const TaskName = "Changing Color";
|
||||
|
@ -92,7 +92,7 @@ const ColorToolForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
|||
initialValues: {
|
||||
color: "",
|
||||
},
|
||||
validationRules: {
|
||||
validate: {
|
||||
color: (c) => colorOptions.find((op) => op.value === c) !== undefined,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useSubtitleAction } from "@/apis/hooks";
|
|||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { task } from "@/modules/task";
|
||||
import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const TaskName = "Changing Frame Rate";
|
||||
|
@ -25,7 +25,7 @@ const FrameRateForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
|||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
validationRules: {
|
||||
validate: {
|
||||
from: (v) => v > 0,
|
||||
to: (v) => v > 0,
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MultiSelector, Selector } from "@/components/inputs";
|
|||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { GetItemId, useSelectorOptions } from "@/utilities";
|
||||
import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { UseMutationResult } from "react-query";
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isString } from "lodash";
|
||||
import { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
|
@ -112,7 +112,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
|||
validateResult: validator(movie, v),
|
||||
})),
|
||||
},
|
||||
validationRules: {
|
||||
validate: {
|
||||
files: (values) => {
|
||||
return (
|
||||
values.find(
|
||||
|
|
|
@ -6,7 +6,6 @@ import { LOG } from "@/utilities/console";
|
|||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Stack,
|
||||
|
@ -14,7 +13,7 @@ import {
|
|||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import ChipInput from "../inputs/ChipInput";
|
||||
|
@ -49,16 +48,10 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
|
||||
const form = useForm({
|
||||
initialValues: profile,
|
||||
validationRules: {
|
||||
validate: {
|
||||
name: (value) => value.length > 0,
|
||||
items: (value) => value.length > 0,
|
||||
},
|
||||
errorMessages: {
|
||||
items: (
|
||||
<Alert color="yellow" variant="outline">
|
||||
Must contain at lease 1 language
|
||||
</Alert>
|
||||
),
|
||||
items: (value) =>
|
||||
value.length > 0 ? null : "Must contain at lease 1 language",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -239,19 +232,18 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
<Stack>
|
||||
<TextInput label="Name" {...form.getInputProps("name")}></TextInput>
|
||||
<Accordion
|
||||
offsetIcon={false}
|
||||
multiple
|
||||
iconPosition="right"
|
||||
initialItem={0}
|
||||
chevronPosition="right"
|
||||
defaultValue={["Languages"]}
|
||||
styles={(theme) => ({
|
||||
contentInner: {
|
||||
content: {
|
||||
[theme.fn.smallerThan("md")]: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Accordion.Item label="Languages">
|
||||
<Accordion.Item value="Languages">
|
||||
<Stack>
|
||||
{form.errors.items}
|
||||
<SimpleTable
|
||||
|
@ -272,7 +264,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
></Selector>
|
||||
</Stack>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item label="Release Info">
|
||||
<Accordion.Item value="Release Info">
|
||||
<Stack>
|
||||
<ChipInput
|
||||
label="Must contain"
|
||||
|
@ -295,7 +287,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
</Text>
|
||||
</Stack>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item label="Subtitles">
|
||||
<Accordion.Item value="Subtitles">
|
||||
<Stack my="xs">
|
||||
<Switch
|
||||
label="Use Original Format"
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isString } from "lodash";
|
||||
import { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
|
@ -125,7 +125,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
|||
validateResult: validator(file),
|
||||
})),
|
||||
},
|
||||
validationRules: {
|
||||
validate: {
|
||||
files: (values) =>
|
||||
values.find(
|
||||
(v) =>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { task } from "@/modules/task";
|
|||
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const TaskName = "Changing Time";
|
||||
|
@ -30,7 +30,7 @@ const TimeOffsetForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
|||
sec: 0,
|
||||
ms: 0,
|
||||
},
|
||||
validationRules: {
|
||||
validate: {
|
||||
hour: (v) => v >= 0,
|
||||
min: (v) => v >= 0,
|
||||
sec: (v) => v >= 0,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { task } from "@/modules/task";
|
|||
import { useSelectorOptions } from "@/utilities";
|
||||
import { useEnabledLanguages } from "@/utilities/languages";
|
||||
import { Alert, Button, Divider, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isObject } from "lodash";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { Selector } from "../inputs";
|
||||
|
@ -138,7 +138,7 @@ const TranslationForm: FunctionComponent<Props> = ({
|
|||
initialValues: {
|
||||
language: null as Language.Info | null,
|
||||
},
|
||||
validationRules: {
|
||||
validate: {
|
||||
language: isObject,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "@mantine/core";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
export type ActionProps = ActionIconProps<"button"> & {
|
||||
export type ActionProps = MantineComp<ActionIconProps, "button"> & {
|
||||
icon: IconDefinition;
|
||||
label: string;
|
||||
tooltip?: Omit<TooltipProps, "label" | "children">;
|
||||
|
|
|
@ -26,6 +26,7 @@ const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
|
|||
getCreateLabel={(query) => `Add "${query}"`}
|
||||
onCreate={(query) => {
|
||||
onChange?.([...(value ?? []), query]);
|
||||
return query;
|
||||
}}
|
||||
></MultiSelector>
|
||||
);
|
||||
|
|
43
frontend/src/components/inputs/DropContent.tsx
Normal file
43
frontend/src/components/inputs/DropContent.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
faArrowUp,
|
||||
faFileCirclePlus,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createStyles, Group, Stack, Text } from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const useStyle = createStyles((theme) => {
|
||||
return {
|
||||
container: {
|
||||
pointerEvents: "none",
|
||||
minHeight: 220,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const DropContent: FunctionComponent = () => {
|
||||
const { classes } = useStyle();
|
||||
|
||||
return (
|
||||
<Group position="center" spacing="xl" className={classes.container}>
|
||||
<Dropzone.Idle>
|
||||
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
|
||||
</Dropzone.Idle>
|
||||
<Dropzone.Accept>
|
||||
<FontAwesomeIcon icon={faArrowUp} size="2x" />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<FontAwesomeIcon icon={faXmark} size="2x" />
|
||||
</Dropzone.Reject>
|
||||
<Stack spacing={0}>
|
||||
<Text size="lg">Upload Subtitles</Text>
|
||||
<Text color="dimmed" size="sm">
|
||||
Attach as many files as you like, you will need to select file
|
||||
metadata before uploading
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
};
|
|
@ -1,129 +0,0 @@
|
|||
import {
|
||||
faArrowUp,
|
||||
faFileCirclePlus,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Box, createStyles, Overlay, Stack, Text } from "@mantine/core";
|
||||
import clsx from "clsx";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { DropzoneState } from "react-dropzone";
|
||||
|
||||
const useStyle = createStyles((theme) => {
|
||||
return {
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
inner: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
margin: theme.spacing.md,
|
||||
borderRadius: theme.radius.md,
|
||||
borderWidth: "0.2rem",
|
||||
borderStyle: "dashed",
|
||||
borderColor: theme.colors.gray[7],
|
||||
backgroundColor: theme.fn.rgba(theme.colors.gray[0], 0.4),
|
||||
},
|
||||
accepted: {
|
||||
borderColor: theme.colors.brand[7],
|
||||
backgroundColor: theme.fn.rgba(theme.colors.brand[0], 0.6),
|
||||
},
|
||||
rejected: {
|
||||
borderColor: theme.colors.red[7],
|
||||
backgroundColor: theme.fn.rgba(theme.colors.red[0], 0.9),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export interface DropOverlayProps {
|
||||
state: DropzoneState;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export const DropOverlay: FunctionComponent<DropOverlayProps> = ({
|
||||
state,
|
||||
children,
|
||||
zIndex = 10,
|
||||
}) => {
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragAccept: accepted,
|
||||
isDragReject: rejected,
|
||||
} = state;
|
||||
|
||||
const { classes } = useStyle();
|
||||
|
||||
const visible = isDragActive;
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (accepted) {
|
||||
return faArrowUp;
|
||||
} else if (rejected) {
|
||||
return faXmark;
|
||||
} else {
|
||||
return faFileCirclePlus;
|
||||
}
|
||||
}, [accepted, rejected]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (accepted) {
|
||||
return "Release to Upload";
|
||||
} else if (rejected) {
|
||||
return "Cannot Upload Files";
|
||||
} else {
|
||||
return "Upload Subtitles";
|
||||
}
|
||||
}, [accepted, rejected]);
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (accepted) {
|
||||
return "";
|
||||
} else if (rejected) {
|
||||
return "Some files are invalid";
|
||||
} else {
|
||||
return "Drop to upload";
|
||||
}
|
||||
}, [accepted, rejected]);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: "relative" }} {...getRootProps()}>
|
||||
{/* Fix for some browsers. Some browsers need a input element to trigger the file browser panel */}
|
||||
<input {...getInputProps()} hidden />
|
||||
{visible && (
|
||||
<Box className={classes.container} style={{ zIndex }}>
|
||||
<Stack
|
||||
spacing="xs"
|
||||
className={clsx(classes.inner, {
|
||||
[classes.accepted]: accepted,
|
||||
[classes.rejected]: rejected,
|
||||
})}
|
||||
style={{ zIndex: zIndex + 1 }}
|
||||
>
|
||||
<Box>
|
||||
<FontAwesomeIcon icon={icon} size="3x" />
|
||||
</Box>
|
||||
<Text size="xl">{title}</Text>
|
||||
<Text color="gray" size="sm">
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Overlay zIndex={zIndex}></Overlay>
|
||||
</Box>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
export { default as Action } from "./Action";
|
||||
export * from "./DropOverlay";
|
||||
export * from "./DropContent";
|
||||
export * from "./FileBrowser";
|
||||
export * from "./Selector";
|
||||
|
|
|
@ -237,18 +237,14 @@ const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
|
|||
const { ref, hovered } = useHover();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
opened={hovered}
|
||||
placement="center"
|
||||
position="top"
|
||||
target={
|
||||
<Popover opened={hovered} position="top">
|
||||
<Popover.Target>
|
||||
<Text color={hasIssues ? "yellow" : "green"} ref={ref}>
|
||||
<FontAwesomeIcon
|
||||
icon={hasIssues ? faExclamationCircle : faCheckCircle}
|
||||
></FontAwesomeIcon>
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
</Popover.Target>
|
||||
<Group align="flex-start" spacing="xl">
|
||||
<Stack align="flex-start" spacing="xs">
|
||||
<Text color="green">
|
||||
|
|
|
@ -2,19 +2,18 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, ButtonProps, Text } from "@mantine/core";
|
||||
import {
|
||||
ComponentProps,
|
||||
FunctionComponent,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type ToolboxButtonProps = Omit<
|
||||
ButtonProps<"button">,
|
||||
"color" | "variant" | "leftIcon"
|
||||
> & {
|
||||
icon: IconDefinition;
|
||||
children: string;
|
||||
};
|
||||
type ToolboxButtonProps = Omit<ButtonProps, "color" | "variant" | "leftIcon"> &
|
||||
Omit<ComponentProps<"button">, "ref"> & {
|
||||
icon: IconDefinition;
|
||||
children: string;
|
||||
};
|
||||
|
||||
const ToolboxButton: FunctionComponent<ToolboxButtonProps> = ({
|
||||
icon,
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
Stack,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const Authentication: FunctionComponent = () => {
|
||||
|
@ -33,7 +33,11 @@ const Authentication: FunctionComponent = () => {
|
|||
src={`${Environment.baseUrl}/images/logo128.png`}
|
||||
></Avatar>
|
||||
<Divider></Divider>
|
||||
<form onSubmit={form.onSubmit(login)}>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
login(values);
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
placeholder="Username"
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useEpisodeSubtitleModification } from "@/apis/hooks";
|
|||
import Language from "@/components/bazarr/Language";
|
||||
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
|
||||
import { task, TaskGroup } from "@/modules/task";
|
||||
import { Badge, FloatingTooltip, MantineColor } from "@mantine/core";
|
||||
import { Badge, MantineColor, Tooltip } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
|
@ -49,11 +49,20 @@ export const Subtitle: FunctionComponent<Props> = ({
|
|||
return list;
|
||||
}, [episodeId, subtitle.code2, subtitle.path]);
|
||||
|
||||
const ctx = (
|
||||
<Badge color={color}>
|
||||
<Language.Text value={subtitle} long={false}></Language.Text>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
return <Tooltip.Floating label="Embedded Subtitle">{ctx}</Tooltip.Floating>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubtitleToolsMenu
|
||||
menu={{
|
||||
trigger: "hover",
|
||||
opened: disabled ? false : undefined,
|
||||
onOpen: () => setOpen(true),
|
||||
onClose: () => setOpen(false),
|
||||
}}
|
||||
|
@ -93,11 +102,7 @@ export const Subtitle: FunctionComponent<Props> = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<FloatingTooltip label="Embedded Subtitle" disabled={!disabled}>
|
||||
<Badge color={color}>
|
||||
<Language.Text value={subtitle} long={false}></Language.Text>
|
||||
</Badge>
|
||||
</FloatingTooltip>
|
||||
{ctx}
|
||||
</SubtitleToolsMenu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
useSeriesById,
|
||||
useSeriesModification,
|
||||
} from "@/apis/hooks";
|
||||
import { DropOverlay, Toolbox } from "@/components";
|
||||
import { DropContent, Toolbox } from "@/components";
|
||||
import { QueryOverlay } from "@/components/async";
|
||||
import { ItemEditModal } from "@/components/forms/ItemEditForm";
|
||||
import { SeriesUploadModal } from "@/components/forms/SeriesUploadForm";
|
||||
|
@ -13,6 +13,7 @@ import { SubtitleToolsModal } from "@/components/modals";
|
|||
import { useModals } from "@/modules/modals";
|
||||
import { notification, task, TaskGroup } from "@/modules/task";
|
||||
import ItemOverview from "@/pages/views/ItemOverview";
|
||||
import { RouterNames } from "@/Router/RouterNames";
|
||||
import { useLanguageProfileBy } from "@/utilities/languages";
|
||||
import {
|
||||
faAdjust,
|
||||
|
@ -24,10 +25,10 @@ import {
|
|||
faWrench,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Container, Group, Stack } from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { FileRejection, useDropzone } from "react-dropzone";
|
||||
import { FunctionComponent, useCallback, useMemo, useRef } from "react";
|
||||
import { Navigate, useParams } from "react-router-dom";
|
||||
import Table from "./table";
|
||||
|
||||
|
@ -67,17 +68,8 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
|||
const hasTask = useIsAnyActionRunning();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(files: File[], rejections: FileRejection[]) => {
|
||||
(files: File[]) => {
|
||||
if (series && profile) {
|
||||
if (rejections.length > 0) {
|
||||
showNotification(
|
||||
notification.warn(
|
||||
"Some files are rejected",
|
||||
`${rejections.length} files are invalid`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
modals.openContextModal(SeriesUploadModal, {
|
||||
files,
|
||||
series,
|
||||
|
@ -94,128 +86,121 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
|||
[modals, profile, series]
|
||||
);
|
||||
|
||||
const dropzone = useDropzone({
|
||||
disabled: profile === undefined,
|
||||
noClick: true,
|
||||
onDrop,
|
||||
});
|
||||
|
||||
useDocumentTitle(`${series?.title ?? "Unknown Series"} - Bazarr (Series)`);
|
||||
|
||||
const openDropzone = useRef<VoidFunction>(null);
|
||||
|
||||
if (isNaN(id) || (isFetched && !series)) {
|
||||
return <Navigate to="/not-found"></Navigate>;
|
||||
return <Navigate to={RouterNames.NotFound}></Navigate>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container px={0} fluid>
|
||||
<QueryOverlay result={seriesQuery}>
|
||||
<DropOverlay state={dropzone}>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={!available || hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(series.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
seriesid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(
|
||||
series.title,
|
||||
TaskGroup.SearchSubtitle,
|
||||
action,
|
||||
{
|
||||
action: "search-missing",
|
||||
seriesid: id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
<Dropzone.FullScreen
|
||||
openRef={openDropzone}
|
||||
active={profile !== undefined}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<DropContent></DropContent>
|
||||
</Dropzone.FullScreen>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={!available || hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(series.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
seriesid: id,
|
||||
});
|
||||
}
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
!available ||
|
||||
hasTask
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(series.title, TaskGroup.SearchSubtitle, action, {
|
||||
action: "search-missing",
|
||||
seriesid: id,
|
||||
});
|
||||
}
|
||||
icon={faBriefcase}
|
||||
onClick={() => {
|
||||
if (episodes) {
|
||||
modals.openContextModal(SubtitleToolsModal, {
|
||||
payload: episodes,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Mass Edit
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}}
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
!available ||
|
||||
hasTask
|
||||
}
|
||||
icon={faBriefcase}
|
||||
onClick={() => {
|
||||
if (episodes) {
|
||||
modals.openContextModal(SubtitleToolsModal, {
|
||||
payload: episodes,
|
||||
});
|
||||
}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={dropzone.open}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: series,
|
||||
mutation,
|
||||
},
|
||||
{ title: series.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Series
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview
|
||||
item={series ?? null}
|
||||
details={details}
|
||||
></ItemOverview>
|
||||
<QueryOverlay result={episodesQuery}>
|
||||
<Table
|
||||
episodes={episodes ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask || !series || series.profileId === null}
|
||||
></Table>
|
||||
</QueryOverlay>
|
||||
</Stack>
|
||||
</DropOverlay>
|
||||
}}
|
||||
>
|
||||
Mass Edit
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={() => openDropzone.current?.()}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: series,
|
||||
mutation,
|
||||
},
|
||||
{ title: series.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Series
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview item={series ?? null} details={details}></ItemOverview>
|
||||
<QueryOverlay result={episodesQuery}>
|
||||
<Table
|
||||
episodes={episodes ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask || !series || series.profileId === null}
|
||||
></Table>
|
||||
</QueryOverlay>
|
||||
</Stack>
|
||||
</QueryOverlay>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -163,6 +163,7 @@ const Table: FunctionComponent<Props> = ({ episodes, profile, disabled }) => {
|
|||
<Action
|
||||
label="Manual Search"
|
||||
disabled={disabled}
|
||||
color="dark"
|
||||
onClick={() => {
|
||||
modals.openContextModal(EpisodeSearchModal, {
|
||||
item: row.original,
|
||||
|
@ -175,6 +176,7 @@ const Table: FunctionComponent<Props> = ({ episodes, profile, disabled }) => {
|
|||
<Action
|
||||
label="History"
|
||||
disabled={disabled}
|
||||
color="dark"
|
||||
onClick={() => {
|
||||
modals.openContextModal(
|
||||
EpisodeHistoryModal,
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
useMovieById,
|
||||
useMovieModification,
|
||||
} from "@/apis/hooks/movies";
|
||||
import { Action, DropOverlay, Toolbox } from "@/components";
|
||||
import { Action, DropContent, Toolbox } from "@/components";
|
||||
import { QueryOverlay } from "@/components/async";
|
||||
import { ItemEditModal } from "@/components/forms/ItemEditForm";
|
||||
import { MovieUploadModal } from "@/components/forms/MovieUploadForm";
|
||||
|
@ -17,6 +17,7 @@ import { MovieSearchModal } from "@/components/modals/ManualSearchModal";
|
|||
import { useModals } from "@/modules/modals";
|
||||
import { notification, task, TaskGroup } from "@/modules/task";
|
||||
import ItemOverview from "@/pages/views/ItemOverview";
|
||||
import { RouterNames } from "@/Router/RouterNames";
|
||||
import { useLanguageProfileBy } from "@/utilities/languages";
|
||||
import {
|
||||
faCloudUploadAlt,
|
||||
|
@ -30,11 +31,11 @@ import {
|
|||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Container, Group, Menu, Stack } from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { isNumber } from "lodash";
|
||||
import { FunctionComponent, useCallback } from "react";
|
||||
import { FileRejection, useDropzone } from "react-dropzone";
|
||||
import { FunctionComponent, useCallback, useRef } from "react";
|
||||
import { Navigate, useParams } from "react-router-dom";
|
||||
import Table from "./table";
|
||||
|
||||
|
@ -80,16 +81,8 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(files: File[], rejections: FileRejection[]) => {
|
||||
(files: File[]) => {
|
||||
if (movie && profile) {
|
||||
if (rejections.length > 0) {
|
||||
showNotification(
|
||||
notification.warn(
|
||||
"Some files are rejected",
|
||||
`${rejections.length} files are invalid`
|
||||
)
|
||||
);
|
||||
}
|
||||
modals.openContextModal(MovieUploadModal, {
|
||||
files,
|
||||
movie,
|
||||
|
@ -110,14 +103,10 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
|
||||
useDocumentTitle(`${movie?.title ?? "Unknown Movie"} - Bazarr (Movies)`);
|
||||
|
||||
const dropzone = useDropzone({
|
||||
disabled: profile === undefined,
|
||||
onDrop,
|
||||
noClick: true,
|
||||
});
|
||||
const openDropzone = useRef<VoidFunction>(null);
|
||||
|
||||
if (isNaN(id) || (isFetched && !movie)) {
|
||||
return <Navigate to="/not-found"></Navigate>;
|
||||
return <Navigate to={RouterNames.NotFound}></Navigate>;
|
||||
}
|
||||
|
||||
const allowEdit = movie?.profileId !== undefined;
|
||||
|
@ -125,88 +114,95 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
return (
|
||||
<Container fluid px={0}>
|
||||
<QueryOverlay result={movieQuery}>
|
||||
<DropOverlay state={dropzone}>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
task.create(movie.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
disabled={!isNumber(movie?.profileId)}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
task.create(movie.title, TaskGroup.SearchSubtitle, action, {
|
||||
action: "search-missing",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faUser}
|
||||
disabled={!isNumber(movie?.profileId) || hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(MovieSearchModal, {
|
||||
item: movie,
|
||||
download,
|
||||
query: useMoviesProvider,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manual
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={!allowEdit || movie.profileId === null || hasTask}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={dropzone.open}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: movie,
|
||||
mutation,
|
||||
},
|
||||
{ title: movie.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Movie
|
||||
</Toolbox.Button>
|
||||
<Menu
|
||||
control={
|
||||
<Action
|
||||
label="More Actions"
|
||||
icon={faEllipsis}
|
||||
disabled={hasTask}
|
||||
/>
|
||||
<Dropzone.FullScreen
|
||||
openRef={openDropzone}
|
||||
active={profile !== undefined}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<DropContent></DropContent>
|
||||
</Dropzone.FullScreen>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
task.create(movie.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
>
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
disabled={!isNumber(movie?.profileId)}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
task.create(movie.title, TaskGroup.SearchSubtitle, action, {
|
||||
action: "search-missing",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faUser}
|
||||
disabled={!isNumber(movie?.profileId) || hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(MovieSearchModal, {
|
||||
item: movie,
|
||||
download,
|
||||
query: useMoviesProvider,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manual
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={!allowEdit || movie.profileId === null || hasTask}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={() => openDropzone.current?.()}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: movie,
|
||||
mutation,
|
||||
},
|
||||
{ title: movie.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Movie
|
||||
</Toolbox.Button>
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Action
|
||||
label="More Actions"
|
||||
color="dark"
|
||||
icon={faEllipsis}
|
||||
disabled={hasTask}
|
||||
/>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faToolbox} />}
|
||||
onClick={() => {
|
||||
|
@ -229,18 +225,18 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
>
|
||||
History
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview item={movie ?? null} details={[]}></ItemOverview>
|
||||
<Table
|
||||
movie={movie ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask}
|
||||
></Table>
|
||||
</Stack>
|
||||
</DropOverlay>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview item={movie ?? null} details={[]}></ItemOverview>
|
||||
<Table
|
||||
movie={movie ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask}
|
||||
></Table>
|
||||
</Stack>
|
||||
</QueryOverlay>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -42,7 +42,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
|
|||
Cell: ({ value }) => {
|
||||
const { classes } = useTableStyles();
|
||||
|
||||
const props: TextProps<"div"> = {
|
||||
const props: TextProps = {
|
||||
className: classes.primary,
|
||||
};
|
||||
|
||||
|
@ -162,6 +162,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
|
|||
<Action
|
||||
label="Subtitle Actions"
|
||||
disabled={isSubtitleTrack(path)}
|
||||
color="dark"
|
||||
icon={faEllipsis}
|
||||
></Action>
|
||||
</SubtitleToolsMenu>
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
} from "@/components";
|
||||
import { Language } from "@/components/bazarr";
|
||||
import { useSelectorOptions } from "@/utilities";
|
||||
import { InputWrapper } from "@mantine/core";
|
||||
import { Input } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { useLatestEnabledLanguages, useLatestProfiles } from ".";
|
||||
import { Selector, SelectorProps } from "../components";
|
||||
|
@ -28,7 +28,7 @@ export const LanguageSelector: FunctionComponent<
|
|||
const wrappedOptions = useSelectorOptions(options, (value) => value.name);
|
||||
|
||||
return (
|
||||
<InputWrapper label={label}>
|
||||
<Input.Wrapper label={label}>
|
||||
<MultiSelector
|
||||
{...wrappedOptions}
|
||||
value={enabled}
|
||||
|
@ -37,7 +37,7 @@ export const LanguageSelector: FunctionComponent<
|
|||
setValue(val, settingKey, location);
|
||||
}}
|
||||
></MultiSelector>
|
||||
</InputWrapper>
|
||||
</Input.Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ import {
|
|||
Stack,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isObject } from "lodash";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { Card } from "../components";
|
||||
|
@ -42,8 +43,8 @@ const NotificationForm: FunctionComponent<Props> = ({
|
|||
selection: payload,
|
||||
url: payload?.url ?? "",
|
||||
},
|
||||
validationRules: {
|
||||
selection: (value) => value !== null,
|
||||
validate: {
|
||||
selection: isObject,
|
||||
url: (value) => value.trim() !== "",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
Stack,
|
||||
Text as MantineText,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { capitalize } from "lodash";
|
||||
import {
|
||||
forwardRef,
|
||||
|
@ -271,7 +271,12 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
|
|||
<Button hidden={!payload} color="red" onClick={deletePayload}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button disabled={!canSave} onClick={form.onSubmit(submit)}>
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
submit(form.values);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
|
|
|
@ -7,7 +7,8 @@ import { usePrompt } from "@/utilities/routers";
|
|||
import { useUpdateLocalStorage } from "@/utilities/storage";
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Badge, Container, Group, LoadingOverlay } from "@mantine/core";
|
||||
import { useDocumentTitle, useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
|
||||
import { enabledLanguageKey, languageProfileKey } from "../keys";
|
||||
import { FormContext, FormValues } from "../utilities/FormValues";
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ActionProps as GlobalActionProps } from "@/components/inputs/Action";
|
|||
import ChipInput, { ChipInputProps } from "@/components/inputs/ChipInput";
|
||||
import { useSliderMarks } from "@/utilities";
|
||||
import {
|
||||
InputWrapper,
|
||||
Input,
|
||||
NumberInput,
|
||||
NumberInputProps,
|
||||
PasswordInput,
|
||||
|
@ -137,14 +137,14 @@ export const Slider: FunctionComponent<SliderProps> = (props) => {
|
|||
const marks = useSliderMarks([min, max]);
|
||||
|
||||
return (
|
||||
<InputWrapper label={label}>
|
||||
<Input.Wrapper label={label}>
|
||||
<MantineSlider
|
||||
{...sliderProps}
|
||||
marks={marks}
|
||||
onChange={update}
|
||||
value={value ?? 0}
|
||||
></MantineSlider>
|
||||
</InputWrapper>
|
||||
</Input.Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { LOG } from "@/utilities/console";
|
||||
import { UseForm } from "@mantine/hooks/lib/use-form/use-form";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
import { createContext, useCallback, useContext, useRef } from "react";
|
||||
|
||||
export const FormContext = createContext<UseForm<FormValues> | null>(null);
|
||||
export const FormContext = createContext<UseFormReturnType<FormValues> | null>(
|
||||
null
|
||||
);
|
||||
|
||||
export function useFormValues() {
|
||||
const context = useContext(FormContext);
|
||||
|
|
|
@ -26,15 +26,14 @@ import {
|
|||
createStyles,
|
||||
Grid,
|
||||
Group,
|
||||
HoverCard,
|
||||
Image,
|
||||
List,
|
||||
MediaQuery,
|
||||
Popover,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useHover } from "@mantine/hooks";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
|
||||
interface Props {
|
||||
|
@ -138,8 +137,6 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
|
|||
return badges;
|
||||
}, [profile, profileItems]);
|
||||
|
||||
const { ref, hovered } = useHover();
|
||||
|
||||
return (
|
||||
<BackgroundImage src={item?.fanart ?? ""}>
|
||||
<Grid
|
||||
|
@ -179,26 +176,23 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
|
|||
{item?.title}
|
||||
</Text>
|
||||
</Title>
|
||||
<Popover
|
||||
opened={hovered}
|
||||
position="bottom"
|
||||
withArrow
|
||||
target={
|
||||
<HoverCard position="bottom" withArrow>
|
||||
<HoverCard.Target>
|
||||
<Text
|
||||
hidden={item?.alternativeTitles.length === 0}
|
||||
color="white"
|
||||
ref={ref}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClone} />
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
{item?.alternativeTitles.map((v, idx) => (
|
||||
<List.Item key={BuildKey(idx, v)}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Popover>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<List>
|
||||
{item?.alternativeTitles.map((v, idx) => (
|
||||
<List.Item key={BuildKey(idx, v)}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</Group>
|
||||
<Group spacing="xs" className={classes.group}>
|
||||
{detailBadges}
|
||||
|
@ -219,17 +213,23 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
type ItemBadgeProps = Omit<BadgeProps<"div">, "leftSection"> & {
|
||||
type ItemBadgeProps = Omit<BadgeProps, "leftSection"> & {
|
||||
icon: IconDefinition;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const ItemBadge: FunctionComponent<ItemBadgeProps> = ({ icon, ...props }) => (
|
||||
const ItemBadge: FunctionComponent<ItemBadgeProps> = ({
|
||||
icon,
|
||||
title,
|
||||
...props
|
||||
}) => (
|
||||
<Badge
|
||||
leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
|
||||
radius="sm"
|
||||
color="dark"
|
||||
size="sm"
|
||||
style={{ textTransform: "none" }}
|
||||
aria-label={title}
|
||||
{...props}
|
||||
></Badge>
|
||||
);
|
||||
|
|
2
frontend/src/types/basic.d.ts
vendored
2
frontend/src/types/basic.d.ts
vendored
|
@ -17,3 +17,5 @@ type SimpleStateType<T> = [
|
|||
];
|
||||
|
||||
type Factory<T> = () => T;
|
||||
|
||||
type MantineComp<T, C> = T & Omit<React.ComponentProps<C>, "ref">;
|
||||
|
|
Loading…
Reference in a new issue