1
0
Fork 0
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:
Liang Yi 2022-09-21 04:50:18 +08:00 committed by GitHub
parent 978b378683
commit a0f8d75f04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2589 additions and 1521 deletions

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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>

View file

@ -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}
>

View file

@ -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);
}
});

View file

@ -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>

View file

@ -0,0 +1,4 @@
export const RouterNames = {
Auth: "/login",
NotFound: "/not-found",
};

View file

@ -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>,
},

View file

@ -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>
);
};

View file

@ -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>;

View file

@ -4,7 +4,7 @@ import { FunctionComponent } from "react";
export type AudioListProps = GroupProps & {
audios: Language.Info[];
badgeProps?: BadgeProps<"div">;
badgeProps?: BadgeProps;
};
const AudioList: FunctionComponent<AudioListProps> = ({

View file

@ -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;
};

View file

@ -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,
},
});

View file

@ -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,
},

View file

@ -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";

View file

@ -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(

View file

@ -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"

View file

@ -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) =>

View file

@ -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,

View file

@ -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,
},
});

View file

@ -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">;

View file

@ -26,6 +26,7 @@ const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
getCreateLabel={(query) => `Add "${query}"`}
onCreate={(query) => {
onChange?.([...(value ?? []), query]);
return query;
}}
></MultiSelector>
);

View 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>
);
};

View file

@ -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>
);
};

View file

@ -1,4 +1,4 @@
export { default as Action } from "./Action";
export * from "./DropOverlay";
export * from "./DropContent";
export * from "./FileBrowser";
export * from "./Selector";

View file

@ -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">

View file

@ -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,

View file

@ -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"

View file

@ -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>
);
};

View file

@ -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>
);

View file

@ -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,

View file

@ -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>
);

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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() !== "",
},
});

View file

@ -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>

View file

@ -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";

View file

@ -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>
);
};

View file

@ -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);

View file

@ -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>
);

View file

@ -17,3 +17,5 @@ type SimpleStateType<T> = [
];
type Factory<T> = () => T;
type MantineComp<T, C> = T & Omit<React.ComponentProps<C>, "ref">;