mirror of
https://github.com/morpheus65535/bazarr.git
synced 2024-09-20 07:25:58 +08:00
Upgraded mantine to v7.x
This commit is contained in:
parent
bb8233b599
commit
be8f2d6d18
747
frontend/package-lock.json
generated
747
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,12 +13,12 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^6.0.21",
|
"@mantine/core": "^7.10.1",
|
||||||
"@mantine/dropzone": "^6.0.21",
|
"@mantine/dropzone": "^7.10.1",
|
||||||
"@mantine/form": "^6.0.21",
|
"@mantine/form": "^7.10.1",
|
||||||
"@mantine/hooks": "^6.0.21",
|
"@mantine/hooks": "^7.10.1",
|
||||||
"@mantine/modals": "^6.0.21",
|
"@mantine/modals": "^7.10.1",
|
||||||
"@mantine/notifications": "^6.0.21",
|
"@mantine/notifications": "^7.10.1",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -53,6 +53,8 @@
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"postcss-preset-mantine": "^1.14.4",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"pretty-quick": "^4.0.0",
|
"pretty-quick": "^4.0.0",
|
||||||
|
|
14
frontend/postcss.config.cjs
Normal file
14
frontend/postcss.config.cjs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-preset-mantine": {},
|
||||||
|
"postcss-simple-vars": {
|
||||||
|
variables: {
|
||||||
|
"mantine-breakpoint-xs": "36em",
|
||||||
|
"mantine-breakpoint-sm": "48em",
|
||||||
|
"mantine-breakpoint-md": "62em",
|
||||||
|
"mantine-breakpoint-lg": "75em",
|
||||||
|
"mantine-breakpoint-xl": "88em",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
9
frontend/src/App/Header.module.scss
Normal file
9
frontend/src/App/Header.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.header {
|
||||||
|
@include light {
|
||||||
|
color: var(--mantine-color-gray-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-dark-0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import { useSystem, useSystemSettings } from "@/apis/hooks";
|
import { useSystem, useSystemSettings } from "@/apis/hooks";
|
||||||
import { Action, Search } from "@/components";
|
import { Action, Search } from "@/components";
|
||||||
import { Layout } from "@/constants";
|
|
||||||
import { useNavbar } from "@/contexts/Navbar";
|
import { useNavbar } from "@/contexts/Navbar";
|
||||||
import { useIsOnline } from "@/contexts/Online";
|
import { useIsOnline } from "@/contexts/Online";
|
||||||
import { Environment, useGotoHomepage } from "@/utilities";
|
import { Environment, useGotoHomepage } from "@/utilities";
|
||||||
|
@ -12,27 +11,16 @@ import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
Anchor,
|
Anchor,
|
||||||
|
AppShell,
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
Burger,
|
Burger,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Header,
|
|
||||||
MediaQuery,
|
|
||||||
Menu,
|
Menu,
|
||||||
createStyles,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
|
import styles from "./Header.module.scss";
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
const headerBackgroundColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[4];
|
|
||||||
return {
|
|
||||||
header: {
|
|
||||||
backgroundColor: headerBackgroundColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const AppHeader: FunctionComponent = () => {
|
const AppHeader: FunctionComponent = () => {
|
||||||
const { data: settings } = useSystemSettings();
|
const { data: settings } = useSystemSettings();
|
||||||
|
@ -47,39 +35,28 @@ const AppHeader: FunctionComponent = () => {
|
||||||
|
|
||||||
const goHome = useGotoHomepage();
|
const goHome = useGotoHomepage();
|
||||||
|
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header p="md" height={Layout.HEADER_HEIGHT} className={classes.header}>
|
<AppShell.Header p="md" className={styles.header}>
|
||||||
<Group position="apart" noWrap>
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group noWrap>
|
<Group wrap="nowrap">
|
||||||
<MediaQuery
|
<Anchor onClick={goHome} visibleFrom="sm">
|
||||||
smallerThan={Layout.MOBILE_BREAKPOINT}
|
<Avatar
|
||||||
styles={{ display: "none" }}
|
alt="brand"
|
||||||
>
|
size={32}
|
||||||
<Anchor onClick={goHome}>
|
src={`${Environment.baseUrl}/images/logo64.png`}
|
||||||
<Avatar
|
></Avatar>
|
||||||
alt="brand"
|
</Anchor>
|
||||||
size={32}
|
<Burger
|
||||||
src={`${Environment.baseUrl}/images/logo64.png`}
|
opened={showed}
|
||||||
></Avatar>
|
onClick={() => show(!showed)}
|
||||||
</Anchor>
|
size="sm"
|
||||||
</MediaQuery>
|
hiddenFrom="sm"
|
||||||
<MediaQuery
|
></Burger>
|
||||||
largerThan={Layout.MOBILE_BREAKPOINT}
|
|
||||||
styles={{ display: "none" }}
|
|
||||||
>
|
|
||||||
<Burger
|
|
||||||
opened={showed}
|
|
||||||
onClick={() => show(!showed)}
|
|
||||||
size="sm"
|
|
||||||
></Burger>
|
|
||||||
</MediaQuery>
|
|
||||||
<Badge size="lg" radius="sm">
|
<Badge size="lg" radius="sm">
|
||||||
Bazarr
|
Bazarr
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
<Group spacing="xs" position="right" noWrap>
|
<Group gap="xs" justify="right" wrap="nowrap">
|
||||||
<Search></Search>
|
<Search></Search>
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
|
@ -95,13 +72,13 @@ const AppHeader: FunctionComponent = () => {
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
leftSection={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
||||||
onClick={() => restart()}
|
onClick={() => restart()}
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<FontAwesomeIcon icon={faPowerOff} />}
|
leftSection={<FontAwesomeIcon icon={faPowerOff} />}
|
||||||
onClick={() => shutdown()}
|
onClick={() => shutdown()}
|
||||||
>
|
>
|
||||||
Shutdown
|
Shutdown
|
||||||
|
@ -114,7 +91,7 @@ const AppHeader: FunctionComponent = () => {
|
||||||
</Menu>
|
</Menu>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Header>
|
</AppShell.Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
56
frontend/src/App/Navbar.module.scss
Normal file
56
frontend/src/App/Navbar.module.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
.anchor {
|
||||||
|
border-color: var(--mantine-color-gray-5);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
border-color: var(--mantine-color-dark-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-left: 2px solid $color-brand-4;
|
||||||
|
background-color: var(--mantine-color-gray-1);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
border-left: 2px solid $color-brand-8;
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover {
|
||||||
|
background-color: var(--mantine-color-gray-0);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
background-color: var(--mantine-color-dark-7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: auto;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: var(--mantine-shadow-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.4rem;
|
||||||
|
margin-right: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
background-color: var(--mantine-color-gray-2);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--mantine-color-gray-8);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-gray-5);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import { Action } from "@/components";
|
import { Action } from "@/components";
|
||||||
import { Layout } from "@/constants";
|
|
||||||
import { useNavbar } from "@/contexts/Navbar";
|
import { useNavbar } from "@/contexts/Navbar";
|
||||||
import { useRouteItems } from "@/Router";
|
import { useRouteItems } from "@/Router";
|
||||||
import { CustomRouteObject, Route } from "@/Router/type";
|
import { CustomRouteObject, Route } from "@/Router/type";
|
||||||
|
@ -14,19 +13,19 @@ import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
Anchor,
|
Anchor,
|
||||||
|
AppShell,
|
||||||
Badge,
|
Badge,
|
||||||
Collapse,
|
Collapse,
|
||||||
createStyles,
|
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Navbar as MantineNavbar,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
useComputedColorScheme,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useHover } from "@mantine/hooks";
|
import { useHover } from "@mantine/hooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
FunctionComponent,
|
FunctionComponent,
|
||||||
useContext,
|
useContext,
|
||||||
|
@ -35,6 +34,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom";
|
import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom";
|
||||||
|
import styles from "./Navbar.module.scss";
|
||||||
|
|
||||||
const Selection = createContext<{
|
const Selection = createContext<{
|
||||||
selection: string | null;
|
selection: string | null;
|
||||||
|
@ -97,11 +97,12 @@ function useIsActive(parent: string, route: RouteObject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppNavbar: FunctionComponent = () => {
|
const AppNavbar: FunctionComponent = () => {
|
||||||
const { showed } = useNavbar();
|
|
||||||
const [selection, select] = useState<string | null>(null);
|
const [selection, select] = useState<string | null>(null);
|
||||||
|
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const computedColorScheme = useComputedColorScheme("light");
|
||||||
|
|
||||||
|
const dark = computedColorScheme === "dark";
|
||||||
|
|
||||||
const routes = useRouteItems();
|
const routes = useRouteItems();
|
||||||
|
|
||||||
|
@ -111,23 +112,10 @@ const AppNavbar: FunctionComponent = () => {
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineNavbar
|
<AppShell.Navbar p="xs" className={styles.nav}>
|
||||||
p="xs"
|
|
||||||
hiddenBreakpoint={Layout.MOBILE_BREAKPOINT}
|
|
||||||
hidden={!showed}
|
|
||||||
width={{ [Layout.MOBILE_BREAKPOINT]: Layout.NAVBAR_WIDTH }}
|
|
||||||
styles={(theme) => ({
|
|
||||||
root: {
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme === "light"
|
|
||||||
? theme.colors.gray[2]
|
|
||||||
: theme.colors.dark[6],
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Selection.Provider value={{ selection, select }}>
|
<Selection.Provider value={{ selection, select }}>
|
||||||
<MantineNavbar.Section grow>
|
<AppShell.Section grow>
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
{routes.map((route, idx) => (
|
{routes.map((route, idx) => (
|
||||||
<RouteItem
|
<RouteItem
|
||||||
key={BuildKey("nav", idx)}
|
key={BuildKey("nav", idx)}
|
||||||
|
@ -136,10 +124,10 @@ const AppNavbar: FunctionComponent = () => {
|
||||||
></RouteItem>
|
></RouteItem>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</MantineNavbar.Section>
|
</AppShell.Section>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<MantineNavbar.Section mt="xs">
|
<AppShell.Section mt="xs">
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Action
|
<Action
|
||||||
label="Change Theme"
|
label="Change Theme"
|
||||||
color={dark ? "yellow" : "indigo"}
|
color={dark ? "yellow" : "indigo"}
|
||||||
|
@ -159,9 +147,9 @@ const AppNavbar: FunctionComponent = () => {
|
||||||
></Action>
|
></Action>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
</MantineNavbar.Section>
|
</AppShell.Section>
|
||||||
</Selection.Provider>
|
</Selection.Provider>
|
||||||
</MantineNavbar>
|
</AppShell.Navbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -186,7 +174,7 @@ const RouteItem: FunctionComponent<{
|
||||||
|
|
||||||
if (children !== undefined) {
|
if (children !== undefined) {
|
||||||
const elements = (
|
const elements = (
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
{children.map((child, idx) => (
|
{children.map((child, idx) => (
|
||||||
<RouteItem
|
<RouteItem
|
||||||
parent={link}
|
parent={link}
|
||||||
|
@ -199,7 +187,7 @@ const RouteItem: FunctionComponent<{
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
<NavbarItem
|
<NavbarItem
|
||||||
primary
|
primary
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -244,53 +232,6 @@ const RouteItem: FunctionComponent<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
const borderColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[5] : theme.colors.dark[4];
|
|
||||||
|
|
||||||
const activeBorderColor =
|
|
||||||
theme.colorScheme === "light"
|
|
||||||
? theme.colors.brand[4]
|
|
||||||
: theme.colors.brand[8];
|
|
||||||
|
|
||||||
const activeBackgroundColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[1] : theme.colors.dark[8];
|
|
||||||
|
|
||||||
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%",
|
|
||||||
color: textColor,
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
textDecoration: "none",
|
|
||||||
borderLeft: `2px solid ${borderColor}`,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
backgroundColor: activeBackgroundColor,
|
|
||||||
borderLeft: `2px solid ${activeBorderColor}`,
|
|
||||||
boxShadow: theme.shadows.xs,
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
backgroundColor: hoverBackgroundColor,
|
|
||||||
},
|
|
||||||
icon: { width: "1.4rem", marginRight: theme.spacing.xs },
|
|
||||||
badge: {
|
|
||||||
marginLeft: "auto",
|
|
||||||
textDecoration: "none",
|
|
||||||
boxShadow: theme.shadows.xs,
|
|
||||||
color: textColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface NavbarItemProps {
|
interface NavbarItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
@ -308,8 +249,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
||||||
onClick,
|
onClick,
|
||||||
primary = false,
|
primary = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
const { show } = useNavbar();
|
const { show } = useNavbar();
|
||||||
|
|
||||||
const { ref, hovered } = useHover();
|
const { ref, hovered } = useHover();
|
||||||
|
@ -335,9 +274,9 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
||||||
}}
|
}}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
clsx(
|
clsx(
|
||||||
clsx(classes.anchor, {
|
clsx(styles.anchor, {
|
||||||
[classes.active]: isActive,
|
[styles.active]: isActive,
|
||||||
[classes.hover]: hovered,
|
[styles.hover]: hovered,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -347,18 +286,19 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
||||||
inline
|
inline
|
||||||
p="xs"
|
p="xs"
|
||||||
size="sm"
|
size="sm"
|
||||||
weight={primary ? "bold" : "normal"}
|
fw={primary ? "bold" : "normal"}
|
||||||
className={classes.text}
|
className={styles.text}
|
||||||
|
span
|
||||||
>
|
>
|
||||||
{icon && (
|
{icon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classes.icon}
|
className={styles.icon}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
></FontAwesomeIcon>
|
></FontAwesomeIcon>
|
||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
{shouldHideBadge === false && (
|
{!shouldHideBadge && (
|
||||||
<Badge className={classes.badge} radius="xs">
|
<Badge className={styles.badge} radius="xs">
|
||||||
{badge}
|
{badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
39
frontend/src/App/ThemeLoader.tsx
Normal file
39
frontend/src/App/ThemeLoader.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { MantineColorScheme, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { useSystemSettings } from "@/apis/hooks";
|
||||||
|
|
||||||
|
const ThemeProvider = () => {
|
||||||
|
const [localScheme, setLocalScheme] = useState<MantineColorScheme | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
const settings = useSystemSettings();
|
||||||
|
|
||||||
|
const settingsColorScheme = settings.data?.general
|
||||||
|
.theme as MantineColorScheme;
|
||||||
|
|
||||||
|
const setScheme = useCallback(
|
||||||
|
(colorScheme: MantineColorScheme) => {
|
||||||
|
setColorScheme(colorScheme);
|
||||||
|
},
|
||||||
|
[setColorScheme],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settingsColorScheme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localScheme === settingsColorScheme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScheme(settingsColorScheme);
|
||||||
|
setLocalScheme(settingsColorScheme);
|
||||||
|
}, [settingsColorScheme, setScheme, localScheme]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
61
frontend/src/App/ThemeProvider.tsx
Normal file
61
frontend/src/App/ThemeProvider.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
AppShell,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
createTheme,
|
||||||
|
MantineProvider,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { FunctionComponent, PropsWithChildren } from "react";
|
||||||
|
import ThemeLoader from "@/App/ThemeLoader";
|
||||||
|
import "@mantine/core/styles.layer.css";
|
||||||
|
import "@mantine/notifications/styles.layer.css";
|
||||||
|
import styleVars from "@/assets/_variables.module.scss";
|
||||||
|
import buttonClasses from "@/assets/button.module.scss";
|
||||||
|
import actionIconClasses from "@/assets/action_icon.module.scss";
|
||||||
|
import appShellClasses from "@/assets/app_shell.module.scss";
|
||||||
|
import badgeClasses from "@/assets/badge.module.scss";
|
||||||
|
|
||||||
|
const themeProvider = createTheme({
|
||||||
|
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
|
||||||
|
colors: {
|
||||||
|
brand: [
|
||||||
|
styleVars.colorBrand0,
|
||||||
|
styleVars.colorBrand1,
|
||||||
|
styleVars.colorBrand2,
|
||||||
|
styleVars.colorBrand3,
|
||||||
|
styleVars.colorBrand4,
|
||||||
|
styleVars.colorBrand5,
|
||||||
|
styleVars.colorBrand6,
|
||||||
|
styleVars.colorBrand7,
|
||||||
|
styleVars.colorBrand8,
|
||||||
|
styleVars.colorBrand9,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryColor: "brand",
|
||||||
|
components: {
|
||||||
|
ActionIcon: ActionIcon.extend({
|
||||||
|
classNames: actionIconClasses,
|
||||||
|
}),
|
||||||
|
AppShell: AppShell.extend({
|
||||||
|
classNames: appShellClasses,
|
||||||
|
}),
|
||||||
|
Badge: Badge.extend({
|
||||||
|
classNames: badgeClasses,
|
||||||
|
}),
|
||||||
|
Button: Button.extend({
|
||||||
|
classNames: buttonClasses,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<MantineProvider theme={themeProvider} defaultColorScheme="auto">
|
||||||
|
<ThemeLoader />
|
||||||
|
{children}
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
|
@ -1,7 +1,6 @@
|
||||||
import AppNavbar from "@/App/Navbar";
|
import AppNavbar from "@/App/Navbar";
|
||||||
import { RouterNames } from "@/Router/RouterNames";
|
import { RouterNames } from "@/Router/RouterNames";
|
||||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import { Layout } from "@/constants";
|
|
||||||
import NavbarProvider from "@/contexts/Navbar";
|
import NavbarProvider from "@/contexts/Navbar";
|
||||||
import OnlineProvider from "@/contexts/Online";
|
import OnlineProvider from "@/contexts/Online";
|
||||||
import { notification } from "@/modules/task";
|
import { notification } from "@/modules/task";
|
||||||
|
@ -13,6 +12,7 @@ import { showNotification } from "@mantine/notifications";
|
||||||
import { FunctionComponent, useEffect, useState } from "react";
|
import { FunctionComponent, useEffect, useState } from "react";
|
||||||
import { Outlet, useNavigate } from "react-router-dom";
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
import AppHeader from "./Header";
|
import AppHeader from "./Header";
|
||||||
|
import styleVars from "@/assets/_variables.module.scss";
|
||||||
|
|
||||||
const App: FunctionComponent = () => {
|
const App: FunctionComponent = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -55,13 +55,19 @@ const App: FunctionComponent = () => {
|
||||||
<NavbarProvider value={{ showed: navbar, show: setNavbar }}>
|
<NavbarProvider value={{ showed: navbar, show: setNavbar }}>
|
||||||
<OnlineProvider value={{ online, setOnline }}>
|
<OnlineProvider value={{ online, setOnline }}>
|
||||||
<AppShell
|
<AppShell
|
||||||
navbarOffsetBreakpoint={Layout.MOBILE_BREAKPOINT}
|
navbar={{
|
||||||
header={<AppHeader></AppHeader>}
|
width: styleVars.navBarWidth,
|
||||||
navbar={<AppNavbar></AppNavbar>}
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !navbar },
|
||||||
|
}}
|
||||||
|
header={{ height: { base: styleVars.headerHeight } }}
|
||||||
padding={0}
|
padding={0}
|
||||||
fixed
|
|
||||||
>
|
>
|
||||||
<Outlet></Outlet>
|
<AppHeader></AppHeader>
|
||||||
|
<AppNavbar></AppNavbar>
|
||||||
|
<AppShell.Main>
|
||||||
|
<Outlet></Outlet>
|
||||||
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</OnlineProvider>
|
</OnlineProvider>
|
||||||
</NavbarProvider>
|
</NavbarProvider>
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { useSystemSettings } from "@/apis/hooks";
|
|
||||||
import {
|
|
||||||
ColorScheme,
|
|
||||||
ColorSchemeProvider,
|
|
||||||
createEmotionCache,
|
|
||||||
MantineProvider,
|
|
||||||
MantineThemeOverride,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useColorScheme } from "@mantine/hooks";
|
|
||||||
import {
|
|
||||||
FunctionComponent,
|
|
||||||
PropsWithChildren,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
const theme: MantineThemeOverride = {
|
|
||||||
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
|
|
||||||
colors: {
|
|
||||||
brand: [
|
|
||||||
"#F8F0FC",
|
|
||||||
"#F3D9FA",
|
|
||||||
"#EEBEFA",
|
|
||||||
"#E599F7",
|
|
||||||
"#DA77F2",
|
|
||||||
"#CC5DE8",
|
|
||||||
"#BE4BDB",
|
|
||||||
"#AE3EC9",
|
|
||||||
"#9C36B5",
|
|
||||||
"#862E9C",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
primaryColor: "brand",
|
|
||||||
};
|
|
||||||
|
|
||||||
function useAutoColorScheme() {
|
|
||||||
const settings = useSystemSettings();
|
|
||||||
const settingsColorScheme = settings.data?.general.theme;
|
|
||||||
|
|
||||||
let preferredColorScheme: ColorScheme = useColorScheme();
|
|
||||||
switch (settingsColorScheme) {
|
|
||||||
case "light":
|
|
||||||
preferredColorScheme = "light" as ColorScheme;
|
|
||||||
break;
|
|
||||||
case "dark":
|
|
||||||
preferredColorScheme = "dark" as ColorScheme;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [colorScheme, setColorScheme] = useState(preferredColorScheme);
|
|
||||||
|
|
||||||
// automatically switch dark/light theme
|
|
||||||
useEffect(() => {
|
|
||||||
setColorScheme(preferredColorScheme);
|
|
||||||
}, [preferredColorScheme]);
|
|
||||||
|
|
||||||
const toggleColorScheme = useCallback((value?: ColorScheme) => {
|
|
||||||
setColorScheme((scheme) => value || (scheme === "dark" ? "light" : "dark"));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { colorScheme, setColorScheme, toggleColorScheme };
|
|
||||||
}
|
|
||||||
|
|
||||||
const emotionCache = createEmotionCache({ key: "bazarr" });
|
|
||||||
|
|
||||||
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
|
||||||
const { colorScheme, toggleColorScheme } = useAutoColorScheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ColorSchemeProvider
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
toggleColorScheme={toggleColorScheme}
|
|
||||||
>
|
|
||||||
<MantineProvider
|
|
||||||
withGlobalStyles
|
|
||||||
withNormalizeCSS
|
|
||||||
theme={{ colorScheme, ...theme }}
|
|
||||||
emotionCache={emotionCache}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MantineProvider>
|
|
||||||
</ColorSchemeProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ThemeProvider;
|
|
|
@ -53,7 +53,9 @@ import Redirector from "./Redirector";
|
||||||
import { RouterNames } from "./RouterNames";
|
import { RouterNames } from "./RouterNames";
|
||||||
import { CustomRouteObject } from "./type";
|
import { CustomRouteObject } from "./type";
|
||||||
|
|
||||||
const HistoryStats = lazy(() => import("@/pages/History/Statistics"));
|
const HistoryStats = lazy(
|
||||||
|
() => import("@/pages/History/Statistics/HistoryStats"),
|
||||||
|
);
|
||||||
const SystemStatusView = lazy(() => import("@/pages/System/Status"));
|
const SystemStatusView = lazy(() => import("@/pages/System/Status"));
|
||||||
|
|
||||||
function useRoutes(): CustomRouteObject[] {
|
function useRoutes(): CustomRouteObject[] {
|
||||||
|
|
40
frontend/src/assets/_bazarr.scss
Normal file
40
frontend/src/assets/_bazarr.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
$color-brand-0: #f8f0fc;
|
||||||
|
$color-brand-1: #f3d9fa;
|
||||||
|
$color-brand-2: #eebefa;
|
||||||
|
$color-brand-3: #e599f7;
|
||||||
|
$color-brand-4: #da77f2;
|
||||||
|
$color-brand-5: #cc5de8;
|
||||||
|
$color-brand-6: #be4bdb;
|
||||||
|
$color-brand-7: #ae3ec9;
|
||||||
|
$color-brand-8: #9c36b5;
|
||||||
|
$color-brand-9: #862e9c;
|
||||||
|
|
||||||
|
$header-height: 64px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.table-long-break {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-primary {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
font-size: var(--mantine-font-size-sm);
|
||||||
|
|
||||||
|
@include smaller-than($mantine-breakpoint-sm) {
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-no-wrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-select {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
@include smaller-than($mantine-breakpoint-sm) {
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
frontend/src/assets/_mantine.scss
Normal file
61
frontend/src/assets/_mantine.scss
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
@use "sass:math";
|
||||||
|
|
||||||
|
$mantine-breakpoint-xs: "36em";
|
||||||
|
$mantine-breakpoint-sm: "48em";
|
||||||
|
$mantine-breakpoint-md: "62em";
|
||||||
|
$mantine-breakpoint-lg: "75em";
|
||||||
|
$mantine-breakpoint-xl: "88em";
|
||||||
|
|
||||||
|
@function rem($value) {
|
||||||
|
@return #{math.div(math.div($value, $value * 0 + 1), 16)}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
[data-mantine-color-scheme="light"] & {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
[data-mantine-color-scheme="dark"] & {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
&:active {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin smaller-than($breakpoint) {
|
||||||
|
@media (max-width: $breakpoint) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin larger-than($breakpoint) {
|
||||||
|
@media (min-width: $breakpoint) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin rtl {
|
||||||
|
[dir="rtl"] & {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin ltr {
|
||||||
|
[dir="ltr"] & {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
18
frontend/src/assets/_variables.module.scss
Normal file
18
frontend/src/assets/_variables.module.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
$navbar-width: 200;
|
||||||
|
|
||||||
|
:export {
|
||||||
|
colorBrand0: $color-brand-0;
|
||||||
|
colorBrand1: $color-brand-1;
|
||||||
|
colorBrand2: $color-brand-2;
|
||||||
|
colorBrand3: $color-brand-3;
|
||||||
|
colorBrand4: $color-brand-4;
|
||||||
|
colorBrand5: $color-brand-5;
|
||||||
|
colorBrand6: $color-brand-6;
|
||||||
|
colorBrand7: $color-brand-7;
|
||||||
|
colorBrand8: $color-brand-8;
|
||||||
|
colorBrand9: $color-brand-9;
|
||||||
|
|
||||||
|
headerHeight: $header-height;
|
||||||
|
|
||||||
|
navBarWidth: $navbar-width;
|
||||||
|
}
|
14
frontend/src/assets/action_icon.module.scss
Normal file
14
frontend/src/assets/action_icon.module.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@layer mantine {
|
||||||
|
.root {
|
||||||
|
&[data-variant="light"] {
|
||||||
|
color: var(--mantine-color-dark-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light {
|
||||||
|
&[data-variant="light"] {
|
||||||
|
background-color: var(--mantine-color-gray-1);
|
||||||
|
color: var(--mantine-color-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
frontend/src/assets/app_shell.module.scss
Normal file
5
frontend/src/assets/app_shell.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.main {
|
||||||
|
@include dark {
|
||||||
|
background-color: rgb(26, 27, 30);
|
||||||
|
}
|
||||||
|
}
|
8
frontend/src/assets/badge.module.scss
Normal file
8
frontend/src/assets/badge.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.root {
|
||||||
|
background-color: var(--mantine-color-grape-light);
|
||||||
|
|
||||||
|
@include light {
|
||||||
|
color: var(--mantine-color-dark-filled);
|
||||||
|
background-color: var(--mantine-color-grape-light);
|
||||||
|
}
|
||||||
|
}
|
12
frontend/src/assets/button.module.scss
Normal file
12
frontend/src/assets/button.module.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@layer mantine {
|
||||||
|
.root {
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-dark-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-variant="danger"] {
|
||||||
|
background-color: var(--mantine-color-red-9);
|
||||||
|
color: var(--mantine-color-red-0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
frontend/src/components/Search.module.scss
Normal file
9
frontend/src/components/Search.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.result {
|
||||||
|
@include light {
|
||||||
|
color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-gray-1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
Anchor,
|
Anchor,
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
createStyles,
|
ComboboxItem,
|
||||||
SelectItemProps,
|
OptionsFilter,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { forwardRef, FunctionComponent, useMemo, useState } from "react";
|
import { FunctionComponent, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import styles from "./Search.module.scss";
|
||||||
|
|
||||||
type SearchResultItem = {
|
type SearchResultItem = {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -41,36 +42,35 @@ function useSearch(query: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||||
return {
|
const lowercaseSearch = search.toLowerCase();
|
||||||
result: {
|
const trimmedSearch = search.trim();
|
||||||
color:
|
|
||||||
theme.colorScheme === "light"
|
|
||||||
? theme.colors.dark[8]
|
|
||||||
: theme.colors.gray[1],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
type ResultCompProps = SelectItemProps & SearchResultItem;
|
|
||||||
|
|
||||||
const ResultComponent = forwardRef<HTMLDivElement, ResultCompProps>(
|
|
||||||
({ link, value }, ref) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
|
return (options as ComboboxItem[]).filter((option) => {
|
||||||
return (
|
return (
|
||||||
<Anchor
|
option.value.toLowerCase().includes(lowercaseSearch) ||
|
||||||
component={Link}
|
option.value
|
||||||
to={link}
|
.normalize("NFD")
|
||||||
underline={false}
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
className={styles.classes.result}
|
.toLowerCase()
|
||||||
p="sm"
|
.includes(trimmedSearch)
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</Anchor>
|
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const ResultComponent = ({ name, link }: { name: string; link: string }) => {
|
||||||
|
return (
|
||||||
|
<Anchor
|
||||||
|
component={Link}
|
||||||
|
to={link}
|
||||||
|
underline="never"
|
||||||
|
className={styles.result}
|
||||||
|
p="sm"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Anchor>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Search: FunctionComponent = () => {
|
const Search: FunctionComponent = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
@ -79,22 +79,22 @@ const Search: FunctionComponent = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
icon={<FontAwesomeIcon icon={faSearch} />}
|
leftSection={<FontAwesomeIcon icon={faSearch} />}
|
||||||
itemComponent={ResultComponent}
|
renderOption={(input) => (
|
||||||
|
<ResultComponent
|
||||||
|
name={input.option.value}
|
||||||
|
link={
|
||||||
|
results.find((a) => a.value === input.option.value)?.link || "/"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
size="sm"
|
size="sm"
|
||||||
data={results}
|
data={results}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={setQuery}
|
onChange={setQuery}
|
||||||
onBlur={() => setQuery("")}
|
onBlur={() => setQuery("")}
|
||||||
filter={(value, item) =>
|
filter={optionsFilter}
|
||||||
item.value.toLowerCase().includes(value.toLowerCase().trim()) ||
|
|
||||||
item.value
|
|
||||||
.normalize("NFD")
|
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(value.trim())
|
|
||||||
}
|
|
||||||
></Autocomplete>
|
></Autocomplete>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,7 +31,7 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
|
||||||
return <FontAwesomeIcon icon={faListCheck} />;
|
return <FontAwesomeIcon icon={faListCheck} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text color={hasIssues ? "yellow" : "green"}>
|
<Text c={hasIssues ? "yellow" : "green"} span>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={hasIssues ? faExclamationCircle : faCheckCircle}
|
icon={hasIssues ? faExclamationCircle : faCheckCircle}
|
||||||
/>
|
/>
|
||||||
|
@ -48,9 +48,9 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<Group position="left" spacing="xl" noWrap grow>
|
<Group justify="left" gap="xl" wrap="nowrap" grow>
|
||||||
<Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto">
|
<Stack align="flex-start" justify="flex-start" gap="xs" mb="auto">
|
||||||
<Text color="green">
|
<Text c="green">
|
||||||
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
|
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
|
||||||
</Text>
|
</Text>
|
||||||
<List>
|
<List>
|
||||||
|
@ -59,8 +59,8 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto">
|
<Stack align="flex-start" justify="flex-start" gap="xs" mb="auto">
|
||||||
<Text color="yellow">
|
<Text c="yellow">
|
||||||
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
|
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
|
||||||
</Text>
|
</Text>
|
||||||
<List>
|
<List>
|
||||||
|
|
|
@ -148,7 +148,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={tool.key}
|
key={tool.key}
|
||||||
disabled={disabledTools}
|
disabled={disabledTools}
|
||||||
icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
|
leftSection={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tool.modal) {
|
if (tool.modal) {
|
||||||
modals.openContextModal(tool.modal, { selections });
|
modals.openContextModal(tool.modal, { selections });
|
||||||
|
@ -164,7 +164,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
|
||||||
<Menu.Label>Actions</Menu.Label>
|
<Menu.Label>Actions</Menu.Label>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
disabled={selections.length !== 0 || onAction === undefined}
|
disabled={selections.length !== 0 || onAction === undefined}
|
||||||
icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
|
leftSection={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAction?.("search");
|
onAction?.("search");
|
||||||
}}
|
}}
|
||||||
|
@ -174,7 +174,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
disabled={selections.length === 0 || onAction === undefined}
|
disabled={selections.length === 0 || onAction === undefined}
|
||||||
color="red"
|
color="red"
|
||||||
icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
|
leftSection={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: "The following subtitles will be deleted",
|
title: "The following subtitles will be deleted",
|
||||||
|
|
|
@ -13,7 +13,7 @@ const AudioList: FunctionComponent<AudioListProps> = ({
|
||||||
...group
|
...group
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs" {...group}>
|
<Group gap="xs" {...group}>
|
||||||
{audios.map((audio, idx) => (
|
{audios.map((audio, idx) => (
|
||||||
<Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}>
|
<Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}>
|
||||||
{audio.name}
|
{audio.name}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawRender, screen } from "@/tests";
|
import { render, screen } from "@/tests";
|
||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
import { Language } from ".";
|
import { Language } from ".";
|
||||||
|
|
||||||
|
@ -9,13 +9,13 @@ describe("Language text", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should show short text", () => {
|
it("should show short text", () => {
|
||||||
rawRender(<Language.Text value={testLanguage}></Language.Text>);
|
render(<Language.Text value={testLanguage}></Language.Text>);
|
||||||
|
|
||||||
expect(screen.getByText(testLanguage.code2)).toBeDefined();
|
expect(screen.getByText(testLanguage.code2)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show long text", () => {
|
it("should show long text", () => {
|
||||||
rawRender(<Language.Text value={testLanguage} long></Language.Text>);
|
render(<Language.Text value={testLanguage} long></Language.Text>);
|
||||||
|
|
||||||
expect(screen.getByText(testLanguage.name)).toBeDefined();
|
expect(screen.getByText(testLanguage.name)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,7 @@ describe("Language text", () => {
|
||||||
const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true };
|
const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true };
|
||||||
|
|
||||||
it("should show short text with HI", () => {
|
it("should show short text with HI", () => {
|
||||||
rawRender(<Language.Text value={testLanguageWithHi}></Language.Text>);
|
render(<Language.Text value={testLanguageWithHi}></Language.Text>);
|
||||||
|
|
||||||
const expectedText = `${testLanguageWithHi.code2}:HI`;
|
const expectedText = `${testLanguageWithHi.code2}:HI`;
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ describe("Language text", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show long text with HI", () => {
|
it("should show long text with HI", () => {
|
||||||
rawRender(<Language.Text value={testLanguageWithHi} long></Language.Text>);
|
render(<Language.Text value={testLanguageWithHi} long></Language.Text>);
|
||||||
|
|
||||||
const expectedText = `${testLanguageWithHi.name} HI`;
|
const expectedText = `${testLanguageWithHi.name} HI`;
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ describe("Language text", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should show short text with Forced", () => {
|
it("should show short text with Forced", () => {
|
||||||
rawRender(<Language.Text value={testLanguageWithForced}></Language.Text>);
|
render(<Language.Text value={testLanguageWithForced}></Language.Text>);
|
||||||
|
|
||||||
const expectedText = `${testLanguageWithHi.code2}:Forced`;
|
const expectedText = `${testLanguageWithHi.code2}:Forced`;
|
||||||
|
|
||||||
|
@ -52,9 +52,7 @@ describe("Language text", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show long text with Forced", () => {
|
it("should show long text with Forced", () => {
|
||||||
rawRender(
|
render(<Language.Text value={testLanguageWithForced} long></Language.Text>);
|
||||||
<Language.Text value={testLanguageWithForced} long></Language.Text>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const expectedText = `${testLanguageWithHi.name} Forced`;
|
const expectedText = `${testLanguageWithHi.name} Forced`;
|
||||||
|
|
||||||
|
@ -75,7 +73,7 @@ describe("Language list", () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
it("should show all languages", () => {
|
it("should show all languages", () => {
|
||||||
rawRender(<Language.List value={elements}></Language.List>);
|
render(<Language.List value={elements}></Language.List>);
|
||||||
|
|
||||||
elements.forEach((value) => {
|
elements.forEach((value) => {
|
||||||
expect(screen.getByText(value.name)).toBeDefined();
|
expect(screen.getByText(value.name)).toBeDefined();
|
||||||
|
|
|
@ -49,7 +49,7 @@ type LanguageListProps = {
|
||||||
|
|
||||||
const LanguageList: FunctionComponent<LanguageListProps> = ({ value }) => {
|
const LanguageList: FunctionComponent<LanguageListProps> = ({ value }) => {
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
{value.map((v) => (
|
{value.map((v) => (
|
||||||
<Badge key={BuildKey(v.code2, v.code2, v.hi)}>{v.name}</Badge>
|
<Badge key={BuildKey(v.code2, v.code2, v.hi)}>{v.name}</Badge>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -55,15 +55,17 @@ const FrameRateForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group spacing="xs" grow>
|
<Group gap="xs" grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
placeholder="From"
|
placeholder="From"
|
||||||
precision={2}
|
decimalScale={2}
|
||||||
|
fixedDecimalScale
|
||||||
{...form.getInputProps("from")}
|
{...form.getInputProps("from")}
|
||||||
></NumberInput>
|
></NumberInput>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
placeholder="To"
|
placeholder="To"
|
||||||
precision={2}
|
decimalScale={2}
|
||||||
|
fixedDecimalScale
|
||||||
{...form.getInputProps("to")}
|
{...form.getInputProps("to")}
|
||||||
></NumberInput>
|
></NumberInput>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -80,7 +80,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
|
||||||
label="Languages Profile"
|
label="Languages Profile"
|
||||||
></Selector>
|
></Selector>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Group position="right">
|
<Group justify="right">
|
||||||
<Button
|
<Button
|
||||||
disabled={isOverlayVisible}
|
disabled={isOverlayVisible}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useMovieSubtitleModification } from "@/apis/hooks";
|
import { useMovieSubtitleModification } from "@/apis/hooks";
|
||||||
import { useModals, withModal } from "@/modules/modals";
|
import { useModals, withModal } from "@/modules/modals";
|
||||||
import { TaskGroup, task } from "@/modules/task";
|
import { TaskGroup, task } from "@/modules/task";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
||||||
import FormUtils from "@/utilities/form";
|
import FormUtils from "@/utilities/form";
|
||||||
import {
|
import {
|
||||||
|
@ -19,7 +18,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
createStyles,
|
|
||||||
Divider,
|
Divider,
|
||||||
MantineColor,
|
MantineColor,
|
||||||
Stack,
|
Stack,
|
||||||
|
@ -79,21 +77,12 @@ interface Props {
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
return {
|
|
||||||
wrapper: {
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const MovieUploadForm: FunctionComponent<Props> = ({
|
const MovieUploadForm: FunctionComponent<Props> = ({
|
||||||
files,
|
files,
|
||||||
movie,
|
movie,
|
||||||
onComplete,
|
onComplete,
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
const profile = useLanguageProfileBy(movie.profileId);
|
const profile = useLanguageProfileBy(movie.profileId);
|
||||||
|
|
||||||
|
@ -187,7 +176,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextPopover text={value?.messages}>
|
<TextPopover text={value?.messages}>
|
||||||
<Text color={color} inline>
|
<Text c={color} inline>
|
||||||
<FontAwesomeIcon icon={icon}></FontAwesomeIcon>
|
<FontAwesomeIcon icon={icon}></FontAwesomeIcon>
|
||||||
</Text>
|
</Text>
|
||||||
</TextPopover>
|
</TextPopover>
|
||||||
|
@ -199,9 +188,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
||||||
id: "filename",
|
id: "filename",
|
||||||
accessor: "file",
|
accessor: "file",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-primary">{value.name}</Text>;
|
||||||
|
|
||||||
return <Text className={classes.primary}>{value.name}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -236,11 +223,10 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
||||||
Header: "Language",
|
Header: "Language",
|
||||||
accessor: "language",
|
accessor: "language",
|
||||||
Cell: ({ row: { original, index }, value }) => {
|
Cell: ({ row: { original, index }, value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
return (
|
return (
|
||||||
<Selector
|
<Selector
|
||||||
{...languageOptions}
|
{...languageOptions}
|
||||||
className={classes.select}
|
className="table-long-break"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(item) => {
|
onChange={(item) => {
|
||||||
action.mutate(index, { ...original, language: item });
|
action.mutate(index, { ...original, language: item });
|
||||||
|
@ -289,7 +275,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
||||||
modals.closeSelf();
|
modals.closeSelf();
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack className={classes.wrapper}>
|
<Stack className="table-long-break">
|
||||||
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
|
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Button type="submit">Upload</Button>
|
<Button type="submit">Upload</Button>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.content {
|
||||||
|
@include smaller-than($mantine-breakpoint-md) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import { Action, Selector, SelectorOption, SimpleTable } from "@/components";
|
import { Action, Selector, SelectorOption, SimpleTable } from "@/components";
|
||||||
import { useModals, withModal } from "@/modules/modals";
|
import { useModals, withModal } from "@/modules/modals";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
||||||
import { LOG } from "@/utilities/console";
|
import { LOG } from "@/utilities/console";
|
||||||
import FormUtils from "@/utilities/form";
|
import FormUtils from "@/utilities/form";
|
||||||
|
@ -19,6 +18,7 @@ import { useForm } from "@mantine/form";
|
||||||
import { FunctionComponent, useCallback, useMemo } from "react";
|
import { FunctionComponent, useCallback, useMemo } from "react";
|
||||||
import { Column } from "react-table";
|
import { Column } from "react-table";
|
||||||
import ChipInput from "../inputs/ChipInput";
|
import ChipInput from "../inputs/ChipInput";
|
||||||
|
import styles from "./ProfileEditForm.module.scss";
|
||||||
|
|
||||||
export const anyCutoff = 65535;
|
export const anyCutoff = 65535;
|
||||||
|
|
||||||
|
@ -162,12 +162,10 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
||||||
[code],
|
[code],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { classes } = useTableStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Selector
|
<Selector
|
||||||
{...languageOptions}
|
{...languageOptions}
|
||||||
className={classes.select}
|
className="table-select"
|
||||||
value={language}
|
value={language}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
@ -260,13 +258,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
||||||
multiple
|
multiple
|
||||||
chevronPosition="right"
|
chevronPosition="right"
|
||||||
defaultValue={["Languages"]}
|
defaultValue={["Languages"]}
|
||||||
styles={(theme) => ({
|
className={styles.content}
|
||||||
content: {
|
|
||||||
[theme.fn.smallerThan("md")]: {
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Accordion.Item value="Languages">
|
<Accordion.Item value="Languages">
|
||||||
<Stack>
|
<Stack>
|
||||||
|
@ -275,7 +267,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={form.values.items}
|
data={form.values.items}
|
||||||
></SimpleTable>
|
></SimpleTable>
|
||||||
<Button fullWidth color="light" onClick={addItem}>
|
<Button fullWidth onClick={addItem}>
|
||||||
Add Language
|
Add Language
|
||||||
</Button>
|
</Button>
|
||||||
<Selector
|
<Selector
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
} from "@/apis/hooks";
|
} from "@/apis/hooks";
|
||||||
import { useModals, withModal } from "@/modules/modals";
|
import { useModals, withModal } from "@/modules/modals";
|
||||||
import { task, TaskGroup } from "@/modules/task";
|
import { task, TaskGroup } from "@/modules/task";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
||||||
import FormUtils from "@/utilities/form";
|
import FormUtils from "@/utilities/form";
|
||||||
import {
|
import {
|
||||||
|
@ -23,7 +22,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
createStyles,
|
|
||||||
Divider,
|
Divider,
|
||||||
MantineColor,
|
MantineColor,
|
||||||
Stack,
|
Stack,
|
||||||
|
@ -86,21 +84,12 @@ interface Props {
|
||||||
onComplete?: VoidFunction;
|
onComplete?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
return {
|
|
||||||
wrapper: {
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const SeriesUploadForm: FunctionComponent<Props> = ({
|
const SeriesUploadForm: FunctionComponent<Props> = ({
|
||||||
series,
|
series,
|
||||||
files,
|
files,
|
||||||
onComplete,
|
onComplete,
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const { classes } = useStyles();
|
|
||||||
const episodes = useEpisodesBySeriesId(series.sonarrSeriesId);
|
const episodes = useEpisodesBySeriesId(series.sonarrSeriesId);
|
||||||
const episodeOptions = useSelectorOptions(
|
const episodeOptions = useSelectorOptions(
|
||||||
episodes.data ?? [],
|
episodes.data ?? [],
|
||||||
|
@ -225,8 +214,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
||||||
id: "filename",
|
id: "filename",
|
||||||
accessor: "file",
|
accessor: "file",
|
||||||
Cell: ({ value: { name } }) => {
|
Cell: ({ value: { name } }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-primary">{name}</Text>;
|
||||||
return <Text className={classes.primary}>{name}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -283,11 +271,10 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
||||||
),
|
),
|
||||||
accessor: "language",
|
accessor: "language",
|
||||||
Cell: ({ row: { original, index }, value }) => {
|
Cell: ({ row: { original, index }, value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
return (
|
return (
|
||||||
<Selector
|
<Selector
|
||||||
{...languageOptions}
|
{...languageOptions}
|
||||||
className={classes.select}
|
className="table-select"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(item) => {
|
onChange={(item) => {
|
||||||
action.mutate(index, { ...original, language: item });
|
action.mutate(index, { ...original, language: item });
|
||||||
|
@ -301,12 +288,11 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
||||||
Header: "Episode",
|
Header: "Episode",
|
||||||
accessor: "episode",
|
accessor: "episode",
|
||||||
Cell: ({ value, row }) => {
|
Cell: ({ value, row }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
return (
|
return (
|
||||||
<Selector
|
<Selector
|
||||||
{...episodeOptions}
|
{...episodeOptions}
|
||||||
searchable
|
searchable
|
||||||
className={classes.select}
|
className="table-select"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(item) => {
|
onChange={(item) => {
|
||||||
action.mutate(row.index, { ...row.original, episode: item });
|
action.mutate(row.index, { ...row.original, episode: item });
|
||||||
|
@ -368,7 +354,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
||||||
modals.closeSelf();
|
modals.closeSelf();
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack className={classes.wrapper}>
|
<Stack className="table-long-break">
|
||||||
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
|
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Button type="submit">Upload</Button>
|
<Button type="submit">Upload</Button>
|
||||||
|
|
|
@ -14,10 +14,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core";
|
import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import { Selector, SelectorOption } from "../inputs";
|
import { GroupedSelector, Selector } from "../inputs";
|
||||||
|
|
||||||
const TaskName = "Syncing Subtitle";
|
const TaskName = "Syncing Subtitle";
|
||||||
|
|
||||||
|
interface SelectOptions {
|
||||||
|
group: string;
|
||||||
|
items: { value: string; label: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
function useReferencedSubtitles(
|
function useReferencedSubtitles(
|
||||||
mediaType: "episode" | "movie",
|
mediaType: "episode" | "movie",
|
||||||
mediaId: number,
|
mediaId: number,
|
||||||
|
@ -37,15 +42,21 @@ function useReferencedSubtitles(
|
||||||
|
|
||||||
const mediaData = mediaType === "episode" ? episodeData : movieData;
|
const mediaData = mediaType === "episode" ? episodeData : movieData;
|
||||||
|
|
||||||
const subtitles: { group: string; value: string; label: string }[] = [];
|
const subtitles: SelectOptions[] = [];
|
||||||
|
|
||||||
if (!mediaData.data) {
|
if (!mediaData.data) {
|
||||||
return [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
if (mediaData.data.audio_tracks.length > 0) {
|
if (mediaData.data.audio_tracks.length > 0) {
|
||||||
|
const embeddedAudioGroup: SelectOptions = {
|
||||||
|
group: "Embedded audio tracks",
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
subtitles.push(embeddedAudioGroup);
|
||||||
|
|
||||||
mediaData.data.audio_tracks.forEach((item) => {
|
mediaData.data.audio_tracks.forEach((item) => {
|
||||||
subtitles.push({
|
embeddedAudioGroup.items.push({
|
||||||
group: "Embedded audio tracks",
|
|
||||||
value: item.stream,
|
value: item.stream,
|
||||||
label: `${item.name || item.language} (${item.stream})`,
|
label: `${item.name || item.language} (${item.stream})`,
|
||||||
});
|
});
|
||||||
|
@ -53,9 +64,15 @@ function useReferencedSubtitles(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaData.data.embedded_subtitles_tracks.length > 0) {
|
if (mediaData.data.embedded_subtitles_tracks.length > 0) {
|
||||||
|
const embeddedSubtitlesTrackGroup: SelectOptions = {
|
||||||
|
group: "Embedded subtitles tracks",
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
subtitles.push(embeddedSubtitlesTrackGroup);
|
||||||
|
|
||||||
mediaData.data.embedded_subtitles_tracks.forEach((item) => {
|
mediaData.data.embedded_subtitles_tracks.forEach((item) => {
|
||||||
subtitles.push({
|
embeddedSubtitlesTrackGroup.items.push({
|
||||||
group: "Embedded subtitles tracks",
|
|
||||||
value: item.stream,
|
value: item.stream,
|
||||||
label: `${item.name || item.language} (${item.stream})`,
|
label: `${item.name || item.language} (${item.stream})`,
|
||||||
});
|
});
|
||||||
|
@ -63,10 +80,16 @@ function useReferencedSubtitles(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaData.data.external_subtitles_tracks.length > 0) {
|
if (mediaData.data.external_subtitles_tracks.length > 0) {
|
||||||
|
const externalSubtitlesFilesGroup: SelectOptions = {
|
||||||
|
group: "External Subtitles files",
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
subtitles.push(externalSubtitlesFilesGroup);
|
||||||
|
|
||||||
mediaData.data.external_subtitles_tracks.forEach((item) => {
|
mediaData.data.external_subtitles_tracks.forEach((item) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
subtitles.push({
|
externalSubtitlesFilesGroup.items.push({
|
||||||
group: "External Subtitles files",
|
|
||||||
value: item.path,
|
value: item.path,
|
||||||
label: item.name,
|
label: item.name,
|
||||||
});
|
});
|
||||||
|
@ -105,7 +128,7 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
|
||||||
const mediaId = selections[0].id;
|
const mediaId = selections[0].id;
|
||||||
const subtitlesPath = selections[0].path;
|
const subtitlesPath = selections[0].path;
|
||||||
|
|
||||||
const subtitles: SelectorOption<string>[] = useReferencedSubtitles(
|
const subtitles: SelectOptions[] = useReferencedSubtitles(
|
||||||
mediaType,
|
mediaType,
|
||||||
mediaId,
|
mediaId,
|
||||||
subtitlesPath,
|
subtitlesPath,
|
||||||
|
@ -145,14 +168,14 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
|
||||||
>
|
>
|
||||||
<Text size="sm">{selections.length} subtitles selected</Text>
|
<Text size="sm">{selections.length} subtitles selected</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Selector
|
<GroupedSelector
|
||||||
clearable
|
clearable
|
||||||
disabled={subtitles.length === 0 || selections.length !== 1}
|
disabled={subtitles.length === 0 || selections.length !== 1}
|
||||||
label="Reference"
|
label="Reference"
|
||||||
placeholder="Default: choose automatically within video file"
|
placeholder="Default: choose automatically within video file"
|
||||||
options={subtitles}
|
options={subtitles}
|
||||||
{...form.getInputProps("reference")}
|
{...form.getInputProps("reference")}
|
||||||
></Selector>
|
></GroupedSelector>
|
||||||
<Selector
|
<Selector
|
||||||
clearable
|
clearable
|
||||||
label="Max Offset Seconds"
|
label="Max Offset Seconds"
|
||||||
|
|
|
@ -70,7 +70,7 @@ const TimeOffsetForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group align="end" spacing="xs" noWrap>
|
<Group align="end" gap="xs" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export { default as Search } from "./Search";
|
export { default as Search } from "./Search";
|
||||||
export * from "./inputs";
|
export * from "./inputs";
|
||||||
export * from "./tables";
|
export * from "./tables";
|
||||||
export { default as Toolbox } from "./toolbox";
|
export { default as Toolbox } from "./toolbox/Toolbox";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawRender, screen } from "@/tests";
|
import { render, screen } from "@/tests";
|
||||||
import { faStickyNote } from "@fortawesome/free-regular-svg-icons";
|
import { faStickyNote } from "@fortawesome/free-regular-svg-icons";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, it, vitest } from "vitest";
|
import { describe, it, vitest } from "vitest";
|
||||||
|
@ -9,7 +9,7 @@ const testIcon = faStickyNote;
|
||||||
|
|
||||||
describe("Action button", () => {
|
describe("Action button", () => {
|
||||||
it("should be a button", () => {
|
it("should be a button", () => {
|
||||||
rawRender(<Action icon={testIcon} label={testLabel}></Action>);
|
render(<Action icon={testIcon} label={testLabel}></Action>);
|
||||||
const element = screen.getByRole("button", { name: testLabel });
|
const element = screen.getByRole("button", { name: testLabel });
|
||||||
|
|
||||||
expect(element.getAttribute("type")).toEqual("button");
|
expect(element.getAttribute("type")).toEqual("button");
|
||||||
|
@ -17,7 +17,7 @@ describe("Action button", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show icon", () => {
|
it("should show icon", () => {
|
||||||
rawRender(<Action icon={testIcon} label={testLabel}></Action>);
|
render(<Action icon={testIcon} label={testLabel}></Action>);
|
||||||
// TODO: use getBy...
|
// TODO: use getBy...
|
||||||
const element = screen.getByRole("img", { hidden: true });
|
const element = screen.getByRole("img", { hidden: true });
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ describe("Action button", () => {
|
||||||
|
|
||||||
it("should call on-click event when clicked", async () => {
|
it("should call on-click event when clicked", async () => {
|
||||||
const onClickFn = vitest.fn();
|
const onClickFn = vitest.fn();
|
||||||
rawRender(
|
render(
|
||||||
<Action icon={testIcon} label={testLabel} onClick={onClickFn}></Action>,
|
<Action icon={testIcon} label={testLabel} onClick={onClickFn}></Action>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawRender, screen } from "@/tests";
|
import { render, screen } from "@/tests";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, it, vitest } from "vitest";
|
import { describe, it, vitest } from "vitest";
|
||||||
import ChipInput from "./ChipInput";
|
import ChipInput from "./ChipInput";
|
||||||
|
@ -8,7 +8,7 @@ describe("ChipInput", () => {
|
||||||
|
|
||||||
// TODO: Support default value
|
// TODO: Support default value
|
||||||
it.skip("should works with default value", () => {
|
it.skip("should works with default value", () => {
|
||||||
rawRender(<ChipInput defaultValue={existedValues}></ChipInput>);
|
render(<ChipInput defaultValue={existedValues}></ChipInput>);
|
||||||
|
|
||||||
existedValues.forEach((value) => {
|
existedValues.forEach((value) => {
|
||||||
expect(screen.getByText(value)).toBeDefined();
|
expect(screen.getByText(value)).toBeDefined();
|
||||||
|
@ -16,7 +16,7 @@ describe("ChipInput", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should works with value", () => {
|
it("should works with value", () => {
|
||||||
rawRender(<ChipInput value={existedValues}></ChipInput>);
|
render(<ChipInput value={existedValues}></ChipInput>);
|
||||||
|
|
||||||
existedValues.forEach((value) => {
|
existedValues.forEach((value) => {
|
||||||
expect(screen.getByText(value)).toBeDefined();
|
expect(screen.getByText(value)).toBeDefined();
|
||||||
|
@ -29,9 +29,7 @@ describe("ChipInput", () => {
|
||||||
expect(values).toContain(typedValue);
|
expect(values).toContain(typedValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
rawRender(
|
render(<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>);
|
||||||
<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const element = screen.getByRole("searchbox");
|
const element = screen.getByRole("searchbox");
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,29 @@
|
||||||
import { useSelectorOptions } from "@/utilities";
|
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import { MultiSelector, MultiSelectorProps } from "./Selector";
|
import { TagsInput } from "@mantine/core";
|
||||||
|
|
||||||
export type ChipInputProps = Omit<
|
export interface ChipInputProps {
|
||||||
MultiSelectorProps<string>,
|
defaultValue?: string[] | undefined;
|
||||||
| "searchable"
|
value?: readonly string[] | null;
|
||||||
| "creatable"
|
label?: string;
|
||||||
| "getCreateLabel"
|
onChange?: (value: string[]) => void;
|
||||||
| "onCreate"
|
}
|
||||||
| "options"
|
|
||||||
| "getkey"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
|
|
||||||
const { value, onChange } = props;
|
|
||||||
|
|
||||||
const options = useSelectorOptions(value ?? [], (v) => v);
|
|
||||||
|
|
||||||
|
const ChipInput: FunctionComponent<ChipInputProps> = ({
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
}: ChipInputProps) => {
|
||||||
|
// TODO: Replace with our own custom implementation instead of just using the
|
||||||
|
// built-in TagsInput. https://mantine.dev/combobox/?e=MultiSelectCreatable
|
||||||
return (
|
return (
|
||||||
<MultiSelector
|
<TagsInput
|
||||||
{...props}
|
defaultValue={defaultValue}
|
||||||
{...options}
|
label={label}
|
||||||
creatable
|
value={value ? value?.map((v) => v) : []}
|
||||||
searchable
|
onChange={onChange}
|
||||||
getCreateLabel={(query) => `Add "${query}"`}
|
clearable
|
||||||
onCreate={(query) => {
|
></TagsInput>
|
||||||
onChange?.([...(value ?? []), query]);
|
|
||||||
return query;
|
|
||||||
}}
|
|
||||||
buildOption={(value) => value}
|
|
||||||
></MultiSelector>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
4
frontend/src/components/inputs/DropContent.module.scss
Normal file
4
frontend/src/components/inputs/DropContent.module.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.container {
|
||||||
|
pointer-events: none;
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
|
@ -4,24 +4,14 @@ import {
|
||||||
faXmark,
|
faXmark,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Group, Stack, Text, createStyles } from "@mantine/core";
|
import { Group, Stack, Text } from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
|
import styles from "./DropContent.module.scss";
|
||||||
const useStyle = createStyles((theme) => {
|
|
||||||
return {
|
|
||||||
container: {
|
|
||||||
pointerEvents: "none",
|
|
||||||
minHeight: 220,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const DropContent: FunctionComponent = () => {
|
export const DropContent: FunctionComponent = () => {
|
||||||
const { classes } = useStyle();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="center" spacing="xl" className={classes.container}>
|
<Group justify="center" gap="xl" className={styles.container}>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
|
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
@ -31,9 +21,9 @@ export const DropContent: FunctionComponent = () => {
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<FontAwesomeIcon icon={faXmark} size="2x" />
|
<FontAwesomeIcon icon={faXmark} size="2x" />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
<Text size="lg">Upload Subtitles</Text>
|
<Text size="lg">Upload Subtitles</Text>
|
||||||
<Text color="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
Attach as many files as you like, you will need to select file
|
Attach as many files as you like, you will need to select file
|
||||||
metadata before uploading
|
metadata before uploading
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { useFileSystem } from "@/apis/hooks";
|
import { useFileSystem } from "@/apis/hooks";
|
||||||
import { faFolder } from "@fortawesome/free-regular-svg-icons";
|
import { faFolder } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Autocomplete, AutocompleteProps } from "@mantine/core";
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
AutocompleteProps,
|
||||||
|
ComboboxItem,
|
||||||
|
OptionsFilter,
|
||||||
|
} from "@mantine/core";
|
||||||
import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react";
|
import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
// TODO: use fortawesome icons
|
// TODO: use fortawesome icons
|
||||||
|
@ -75,24 +80,28 @@ export const FileBrowser: FunctionComponent<FileBrowserProps> = ({
|
||||||
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||||
|
return (options as ComboboxItem[]).filter((option) => {
|
||||||
|
if (search === backKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return option.value.includes(search);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
icon={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
|
leftSection={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
|
||||||
placeholder="Click to start"
|
placeholder="Click to start"
|
||||||
data={data}
|
data={data}
|
||||||
value={value}
|
value={value}
|
||||||
// Temporary solution of infinite dropdown items, fix later
|
// Temporary solution of infinite dropdown items, fix later
|
||||||
limit={NaN}
|
limit={NaN}
|
||||||
maxDropdownHeight={240}
|
maxDropdownHeight={240}
|
||||||
filter={(value, item) => {
|
filter={optionsFilter}
|
||||||
if (item.value === backKey) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return item.value.includes(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val !== backKey) {
|
if (val !== backKey) {
|
||||||
setValue(val);
|
setValue(val);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawRender, screen } from "@/tests";
|
import { render, screen } from "@/tests";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, it, vitest } from "vitest";
|
import { describe, it, vitest } from "vitest";
|
||||||
import { Selector, SelectorOption } from "./Selector";
|
import { Selector, SelectorOption } from "./Selector";
|
||||||
|
@ -18,20 +18,17 @@ const testOptions: SelectorOption<string>[] = [
|
||||||
describe("Selector", () => {
|
describe("Selector", () => {
|
||||||
describe("options", () => {
|
describe("options", () => {
|
||||||
it("should work with the SelectorOption", () => {
|
it("should work with the SelectorOption", () => {
|
||||||
rawRender(
|
render(<Selector name={selectorName} options={testOptions}></Selector>);
|
||||||
<Selector name={selectorName} options={testOptions}></Selector>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: selectorName
|
testOptions.forEach((o) => {
|
||||||
expect(screen.getByRole("searchbox")).toBeDefined();
|
expect(screen.getByText(o.label)).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display when clicked", async () => {
|
it("should display when clicked", async () => {
|
||||||
rawRender(
|
render(<Selector name={selectorName} options={testOptions}></Selector>);
|
||||||
<Selector name={selectorName} options={testOptions}></Selector>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const element = screen.getByRole("searchbox");
|
const element = screen.getByTestId("input-selector");
|
||||||
|
|
||||||
await userEvent.click(element);
|
await userEvent.click(element);
|
||||||
|
|
||||||
|
@ -44,7 +41,7 @@ describe("Selector", () => {
|
||||||
|
|
||||||
it("shouldn't show default value", async () => {
|
it("shouldn't show default value", async () => {
|
||||||
const option = testOptions[0];
|
const option = testOptions[0];
|
||||||
rawRender(
|
render(
|
||||||
<Selector
|
<Selector
|
||||||
name={selectorName}
|
name={selectorName}
|
||||||
options={testOptions}
|
options={testOptions}
|
||||||
|
@ -57,7 +54,7 @@ describe("Selector", () => {
|
||||||
|
|
||||||
it("shouldn't show value", async () => {
|
it("shouldn't show value", async () => {
|
||||||
const option = testOptions[0];
|
const option = testOptions[0];
|
||||||
rawRender(
|
render(
|
||||||
<Selector
|
<Selector
|
||||||
name={selectorName}
|
name={selectorName}
|
||||||
options={testOptions}
|
options={testOptions}
|
||||||
|
@ -75,7 +72,7 @@ describe("Selector", () => {
|
||||||
const mockedFn = vitest.fn((value: string | null) => {
|
const mockedFn = vitest.fn((value: string | null) => {
|
||||||
expect(value).toEqual(clickedOption.value);
|
expect(value).toEqual(clickedOption.value);
|
||||||
});
|
});
|
||||||
rawRender(
|
render(
|
||||||
<Selector
|
<Selector
|
||||||
name={selectorName}
|
name={selectorName}
|
||||||
options={testOptions}
|
options={testOptions}
|
||||||
|
@ -83,13 +80,13 @@ describe("Selector", () => {
|
||||||
></Selector>,
|
></Selector>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const element = screen.getByRole("searchbox");
|
const element = screen.getByTestId("input-selector");
|
||||||
|
|
||||||
await userEvent.click(element);
|
await userEvent.click(element);
|
||||||
|
|
||||||
await userEvent.click(screen.getByText(clickedOption.label));
|
await userEvent.click(screen.getByText(clickedOption.label));
|
||||||
|
|
||||||
expect(mockedFn).toBeCalled();
|
expect(mockedFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -115,7 +112,7 @@ describe("Selector", () => {
|
||||||
const mockedFn = vitest.fn((value: { name: string } | null) => {
|
const mockedFn = vitest.fn((value: { name: string } | null) => {
|
||||||
expect(value).toEqual(clickedOption.value);
|
expect(value).toEqual(clickedOption.value);
|
||||||
});
|
});
|
||||||
rawRender(
|
render(
|
||||||
<Selector
|
<Selector
|
||||||
name={selectorName}
|
name={selectorName}
|
||||||
options={objectOptions}
|
options={objectOptions}
|
||||||
|
@ -124,20 +121,20 @@ describe("Selector", () => {
|
||||||
></Selector>,
|
></Selector>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const element = screen.getByRole("searchbox");
|
const element = screen.getByTestId("input-selector");
|
||||||
|
|
||||||
await userEvent.click(element);
|
await userEvent.click(element);
|
||||||
|
|
||||||
await userEvent.click(screen.getByText(clickedOption.label));
|
await userEvent.click(screen.getByText(clickedOption.label));
|
||||||
|
|
||||||
expect(mockedFn).toBeCalled();
|
expect(mockedFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("placeholder", () => {
|
describe("placeholder", () => {
|
||||||
it("should show when no selection", () => {
|
it("should show when no selection", () => {
|
||||||
const placeholder = "Empty Selection";
|
const placeholder = "Empty Selection";
|
||||||
rawRender(
|
render(
|
||||||
<Selector
|
<Selector
|
||||||
name={selectorName}
|
name={selectorName}
|
||||||
options={testOptions}
|
options={testOptions}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { LOG } from "@/utilities/console";
|
import { LOG } from "@/utilities/console";
|
||||||
import {
|
import {
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxParsedItemGroup,
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
MultiSelectProps,
|
MultiSelectProps,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
|
||||||
SelectProps,
|
SelectProps,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { isNull, isUndefined } from "lodash";
|
import { isNull, isUndefined } from "lodash";
|
||||||
|
@ -14,10 +15,10 @@ export type SelectorOption<T> = Override<
|
||||||
value: T;
|
value: T;
|
||||||
label: string;
|
label: string;
|
||||||
},
|
},
|
||||||
SelectItem
|
ComboboxItem
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type SelectItemWithPayload<T> = SelectItem & {
|
type SelectItemWithPayload<T> = ComboboxItem & {
|
||||||
payload: T;
|
payload: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,6 +35,30 @@ function DefaultKeyBuilder<T>(value: T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GroupedSelectorProps<T> = Override<
|
||||||
|
{
|
||||||
|
options: ComboboxParsedItemGroup[];
|
||||||
|
getkey?: (value: T) => string;
|
||||||
|
},
|
||||||
|
Omit<SelectProps, "data">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function GroupedSelector<T>({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
getkey = DefaultKeyBuilder,
|
||||||
|
...select
|
||||||
|
}: GroupedSelectorProps<T>) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
data-testid="input-selector"
|
||||||
|
comboboxProps={{ withinPortal: true }}
|
||||||
|
data={options}
|
||||||
|
{...select}
|
||||||
|
></Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type SelectorProps<T> = Override<
|
export type SelectorProps<T> = Override<
|
||||||
{
|
{
|
||||||
value?: T | null;
|
value?: T | null;
|
||||||
|
@ -84,7 +109,7 @@ export function Selector<T>({
|
||||||
}, [defaultValue, keyRef]);
|
}, [defaultValue, keyRef]);
|
||||||
|
|
||||||
const wrappedOnChange = useCallback(
|
const wrappedOnChange = useCallback(
|
||||||
(value: string) => {
|
(value: string | null) => {
|
||||||
const payload = data.find((v) => v.value === value)?.payload ?? null;
|
const payload = data.find((v) => v.value === value)?.payload ?? null;
|
||||||
onChange?.(payload);
|
onChange?.(payload);
|
||||||
},
|
},
|
||||||
|
@ -93,7 +118,8 @@ export function Selector<T>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
withinPortal={true}
|
data-testid="input-selector"
|
||||||
|
comboboxProps={{ withinPortal: true }}
|
||||||
data={data}
|
data={data}
|
||||||
defaultValue={wrappedDefaultValue}
|
defaultValue={wrappedDefaultValue}
|
||||||
value={wrappedValue}
|
value={wrappedValue}
|
||||||
|
@ -144,6 +170,7 @@ export function MultiSelector<T>({
|
||||||
() => value && value.map(labelRef.current),
|
() => value && value.map(labelRef.current),
|
||||||
[value],
|
[value],
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrappedDefaultValue = useMemo(
|
const wrappedDefaultValue = useMemo(
|
||||||
() => defaultValue && defaultValue.map(labelRef.current),
|
() => defaultValue && defaultValue.map(labelRef.current),
|
||||||
[defaultValue],
|
[defaultValue],
|
||||||
|
@ -168,6 +195,7 @@ export function MultiSelector<T>({
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
{...select}
|
{...select}
|
||||||
|
hidePickedOptions
|
||||||
value={wrappedValue}
|
value={wrappedValue}
|
||||||
defaultValue={wrappedDefaultValue}
|
defaultValue={wrappedDefaultValue}
|
||||||
onChange={wrappedOnChange}
|
onChange={wrappedOnChange}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { withModal } from "@/modules/modals";
|
import { withModal } from "@/modules/modals";
|
||||||
import { task, TaskGroup } from "@/modules/task";
|
import { task, TaskGroup } from "@/modules/task";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { GetItemId } from "@/utilities";
|
import { GetItemId } from "@/utilities";
|
||||||
import {
|
import {
|
||||||
faCaretDown,
|
faCaretDown,
|
||||||
|
@ -31,9 +30,7 @@ type SupportType = Item.Movie | Item.Episode;
|
||||||
|
|
||||||
interface Props<T extends SupportType> {
|
interface Props<T extends SupportType> {
|
||||||
download: (item: T, result: SearchResultType) => Promise<void>;
|
download: (item: T, result: SearchResultType) => Promise<void>;
|
||||||
query: (
|
query: (id?: number) => UseQueryResult<SearchResultType[] | undefined>;
|
||||||
id?: number,
|
|
||||||
) => UseQueryResult<SearchResultType[] | undefined, unknown>;
|
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +47,8 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||||
|
|
||||||
const search = useCallback(() => {
|
const search = useCallback(() => {
|
||||||
setSearchStarted(true);
|
setSearchStarted(true);
|
||||||
results.refetch();
|
|
||||||
|
void results.refetch();
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
const columns = useMemo<Column<SearchResultType>[]>(
|
const columns = useMemo<Column<SearchResultType>[]>(
|
||||||
|
@ -59,8 +57,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||||
Header: "Score",
|
Header: "Score",
|
||||||
accessor: "score",
|
accessor: "score",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value}%</Text>;
|
||||||
return <Text className={classes.noWrap}>{value}%</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -84,13 +81,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||||
Header: "Provider",
|
Header: "Provider",
|
||||||
accessor: "provider",
|
accessor: "provider",
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const value = row.value;
|
const value = row.value;
|
||||||
const { url } = row.row.original;
|
const { url } = row.row.original;
|
||||||
if (url) {
|
if (url) {
|
||||||
return (
|
return (
|
||||||
<Anchor
|
<Anchor
|
||||||
className={classes.noWrap}
|
className="table-no-wrap"
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
@ -107,7 +103,6 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||||
Header: "Release",
|
Header: "Release",
|
||||||
accessor: "release_info",
|
accessor: "release_info",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
|
@ -116,12 +111,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
return <Text color="dimmed">Cannot get release info</Text>;
|
return <Text c="dimmed">Cannot get release info</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={0} onClick={() => setOpen((o) => !o)}>
|
<Stack gap={0} onClick={() => setOpen((o) => !o)}>
|
||||||
<Text className={classes.primary}>
|
<Text className="table-primary" span>
|
||||||
{value[0]}
|
{value[0]}
|
||||||
{value.length > 1 && (
|
{value.length > 1 && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -141,8 +136,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||||
Header: "Uploader",
|
Header: "Uploader",
|
||||||
accessor: "uploader",
|
accessor: "uploader",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value ?? "-"}</Text>;
|
||||||
return <Text className={classes.noWrap}>{value ?? "-"}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
9
frontend/src/components/tables/BaseTable.module.scss
Normal file
9
frontend/src/components/tables/BaseTable.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { useIsLoading } from "@/contexts";
|
import { useIsLoading } from "@/contexts";
|
||||||
import { usePageSize } from "@/utilities/storage";
|
import { usePageSize } from "@/utilities/storage";
|
||||||
import { Box, createStyles, Skeleton, Table, Text } from "@mantine/core";
|
import { Box, Skeleton, Table, Text } from "@mantine/core";
|
||||||
import { ReactNode, useMemo } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import { HeaderGroup, Row, TableInstance } from "react-table";
|
import { HeaderGroup, Row, TableInstance } from "react-table";
|
||||||
|
import styles from "./BaseTable.module.scss";
|
||||||
|
|
||||||
export type BaseTableProps<T extends object> = TableInstance<T> & {
|
export type BaseTableProps<T extends object> = TableInstance<T> & {
|
||||||
tableStyles?: TableStyleProps<T>;
|
tableStyles?: TableStyleProps<T>;
|
||||||
|
@ -18,37 +19,23 @@ export interface TableStyleProps<T extends object> {
|
||||||
rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>;
|
rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
return {
|
|
||||||
container: {
|
|
||||||
display: "block",
|
|
||||||
maxWidth: "100%",
|
|
||||||
overflowX: "auto",
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
borderCollapse: "collapse",
|
|
||||||
},
|
|
||||||
header: {},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function DefaultHeaderRenderer<T extends object>(
|
function DefaultHeaderRenderer<T extends object>(
|
||||||
headers: HeaderGroup<T>[],
|
headers: HeaderGroup<T>[],
|
||||||
): JSX.Element[] {
|
): JSX.Element[] {
|
||||||
return headers.map((col) => (
|
return headers.map((col) => (
|
||||||
<th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
|
<Table.Th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
|
||||||
{col.render("Header")}
|
{col.render("Header")}
|
||||||
</th>
|
</Table.Th>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null {
|
function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null {
|
||||||
return (
|
return (
|
||||||
<tr {...row.getRowProps()}>
|
<Table.Tr {...row.getRowProps()}>
|
||||||
{row.cells.map((cell) => (
|
{row.cells.map((cell) => (
|
||||||
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
|
<Table.Td {...cell.getCellProps()}>{cell.render("Cell")}</Table.Td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,8 +53,6 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
|
||||||
const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
|
const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
|
||||||
const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
|
const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
|
||||||
|
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
const colCount = useMemo(() => {
|
const colCount = useMemo(() => {
|
||||||
return headerGroups.reduce(
|
return headerGroups.reduce(
|
||||||
(prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev),
|
(prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev),
|
||||||
|
@ -88,19 +73,19 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
|
||||||
body = Array(tableStyles?.placeholder ?? pageSize)
|
body = Array(tableStyles?.placeholder ?? pageSize)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => (
|
.map((_, i) => (
|
||||||
<tr key={i}>
|
<Table.Tr key={i}>
|
||||||
<td colSpan={colCount}>
|
<Table.Td colSpan={colCount}>
|
||||||
<Skeleton height={24}></Skeleton>
|
<Skeleton height={24}></Skeleton>
|
||||||
</td>
|
</Table.Td>
|
||||||
</tr>
|
</Table.Tr>
|
||||||
));
|
));
|
||||||
} else if (empty && tableStyles?.emptyText) {
|
} else if (empty && tableStyles?.emptyText) {
|
||||||
body = (
|
body = (
|
||||||
<tr>
|
<Table.Tr>
|
||||||
<td colSpan={colCount}>
|
<Table.Td colSpan={colCount}>
|
||||||
<Text align="center">{tableStyles.emptyText}</Text>
|
<Text ta="center">{tableStyles.emptyText}</Text>
|
||||||
</td>
|
</Table.Td>
|
||||||
</tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
body = rows.map((row) => {
|
body = rows.map((row) => {
|
||||||
|
@ -110,20 +95,20 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.container}>
|
<Box className={styles.container}>
|
||||||
<Table
|
<Table
|
||||||
className={classes.table}
|
className={styles.table}
|
||||||
striped={tableStyles?.striped ?? true}
|
striped={tableStyles?.striped ?? true}
|
||||||
{...getTableProps()}
|
{...getTableProps()}
|
||||||
>
|
>
|
||||||
<thead className={classes.header} hidden={tableStyles?.hideHeader}>
|
<Table.Thead hidden={tableStyles?.hideHeader}>
|
||||||
{headerGroups.map((headerGroup) => (
|
{headerGroups.map((headerGroup) => (
|
||||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
<Table.Tr {...headerGroup.getHeaderGroupProps()}>
|
||||||
{headersRenderer(headerGroup.headers)}
|
{headersRenderer(headerGroup.headers)}
|
||||||
</tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</Table.Thead>
|
||||||
<tbody {...getTableBodyProps()}>{body}</tbody>
|
<Table.Tbody {...getTableBodyProps()}>{body}</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Box, Text } from "@mantine/core";
|
import { Box, Text, Table } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
HeaderGroup,
|
HeaderGroup,
|
||||||
|
@ -29,8 +29,8 @@ function renderRow<T extends object>(row: Row<T>) {
|
||||||
if (cell) {
|
if (cell) {
|
||||||
const rotation = row.isExpanded ? 90 : undefined;
|
const rotation = row.isExpanded ? 90 : undefined;
|
||||||
return (
|
return (
|
||||||
<tr {...row.getRowProps()}>
|
<Table.Tr {...row.getRowProps()}>
|
||||||
<td {...cell.getCellProps()} colSpan={row.cells.length}>
|
<Table.Td {...cell.getCellProps()} colSpan={row.cells.length}>
|
||||||
<Text {...row.getToggleRowExpandedProps()} p={2}>
|
<Text {...row.getToggleRowExpandedProps()} p={2}>
|
||||||
{cell.render("Cell")}
|
{cell.render("Cell")}
|
||||||
<Box component="span" mx={12}>
|
<Box component="span" mx={12}>
|
||||||
|
@ -40,21 +40,23 @@ function renderRow<T extends object>(row: Row<T>) {
|
||||||
></FontAwesomeIcon>
|
></FontAwesomeIcon>
|
||||||
</Box>
|
</Box>
|
||||||
</Text>
|
</Text>
|
||||||
</td>
|
</Table.Td>
|
||||||
</tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<tr {...row.getRowProps()}>
|
<Table.Tr {...row.getRowProps()}>
|
||||||
{row.cells
|
{row.cells
|
||||||
.filter((cell) => !cell.isPlaceholder)
|
.filter((cell) => !cell.isPlaceholder)
|
||||||
.map((cell) => (
|
.map((cell) => (
|
||||||
<td {...cell.getCellProps()}>{renderCell(cell, row)}</td>
|
<Table.Td {...cell.getCellProps()}>
|
||||||
|
{renderCell(cell, row)}
|
||||||
|
</Table.Td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +66,9 @@ function renderHeaders<T extends object>(
|
||||||
): JSX.Element[] {
|
): JSX.Element[] {
|
||||||
return headers
|
return headers
|
||||||
.filter((col) => !col.isGrouped)
|
.filter((col) => !col.isGrouped)
|
||||||
.map((col) => <th {...col.getHeaderProps()}>{col.render("Header")}</th>);
|
.map((col) => (
|
||||||
|
<Table.Th {...col.getHeaderProps()}>{col.render("Header")}</Table.Th>
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props<T extends object> = Omit<
|
type Props<T extends object> = Omit<
|
||||||
|
|
|
@ -28,7 +28,7 @@ const PageControl: FunctionComponent<Props> = ({
|
||||||
}, [total, goto]);
|
}, [total, goto]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group p={16} position="apart">
|
<Group p={16} justify="apart">
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Show {start} to {end} of {total} entries
|
Show {start} to {end} of {total} entries
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const ToolboxButton: FunctionComponent<ToolboxButtonProps> = ({
|
||||||
<Button
|
<Button
|
||||||
color="dark"
|
color="dark"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
leftIcon={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
|
leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Text size="xs">{children}</Text>
|
<Text size="xs">{children}</Text>
|
||||||
|
|
9
frontend/src/components/toolbox/Toolbox.module.scss
Normal file
9
frontend/src/components/toolbox/Toolbox.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.group {
|
||||||
|
@include light {
|
||||||
|
color: var(--mantine-color-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-dark-5);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,7 @@
|
||||||
import { createStyles, Group } from "@mantine/core";
|
import { Group } from "@mantine/core";
|
||||||
import { FunctionComponent, PropsWithChildren } from "react";
|
import { FunctionComponent, PropsWithChildren } from "react";
|
||||||
import ToolboxButton, { ToolboxMutateButton } from "./Button";
|
import ToolboxButton, { ToolboxMutateButton } from "./Button";
|
||||||
|
import styles from "./Toolbox.module.scss";
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
group: {
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme === "light"
|
|
||||||
? theme.colors.gray[3]
|
|
||||||
: theme.colors.dark[5],
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
|
declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
|
||||||
Button: typeof ToolboxButton;
|
Button: typeof ToolboxButton;
|
||||||
|
@ -17,9 +9,8 @@ declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Toolbox: ToolboxComp = ({ children }) => {
|
const Toolbox: ToolboxComp = ({ children }) => {
|
||||||
const { classes } = useStyles();
|
|
||||||
return (
|
return (
|
||||||
<Group p={12} position="apart" className={classes.group}>
|
<Group p={12} justify="apart" className={styles.group}>
|
||||||
{children}
|
{children}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
|
@ -1,9 +1 @@
|
||||||
import { MantineNumberSize } from "@mantine/core";
|
|
||||||
|
|
||||||
export const GithubRepoRoot = "https://github.com/morpheus65535/bazarr";
|
export const GithubRepoRoot = "https://github.com/morpheus65535/bazarr";
|
||||||
|
|
||||||
export const Layout = {
|
|
||||||
NAVBAR_WIDTH: 200,
|
|
||||||
HEADER_HEIGHT: 64,
|
|
||||||
MOBILE_BREAKPOINT: "sm" as MantineNumberSize,
|
|
||||||
};
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
|
||||||
update: (msg) => {
|
update: (msg) => {
|
||||||
msg
|
msg
|
||||||
.map((message) => notification.info("Notification", message))
|
.map((message) => notification.info("Notification", message))
|
||||||
.forEach(showNotification);
|
.forEach((data) => showNotification(data));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -133,7 +133,7 @@ class TaskDispatcher {
|
||||||
|
|
||||||
public removeProgress(ids: string[]) {
|
public removeProgress(ids: string[]) {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => ids.forEach(hideNotification),
|
() => ids.forEach((id) => hideNotification(id)),
|
||||||
notification.PROGRESS_TIMEOUT,
|
notification.PROGRESS_TIMEOUT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NotificationProps } from "@mantine/notifications";
|
import { NotificationData } from "@mantine/notifications";
|
||||||
|
|
||||||
export const notification = {
|
export const notification = {
|
||||||
info: (title: string, message: string): NotificationProps => {
|
info: (title: string, message: string): NotificationData => {
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
@ -9,7 +9,7 @@ export const notification = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
warn: (title: string, message: string): NotificationProps => {
|
warn: (title: string, message: string): NotificationData => {
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
@ -18,7 +18,7 @@ export const notification = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
error: (title: string, message: string): NotificationProps => {
|
error: (title: string, message: string): NotificationData => {
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
@ -33,7 +33,7 @@ export const notification = {
|
||||||
pending: (
|
pending: (
|
||||||
id: string,
|
id: string,
|
||||||
header: string,
|
header: string,
|
||||||
): NotificationProps & { id: string } => {
|
): NotificationData & { id: string } => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: header,
|
title: header,
|
||||||
|
@ -48,7 +48,7 @@ export const notification = {
|
||||||
body: string,
|
body: string,
|
||||||
current: number,
|
current: number,
|
||||||
total: number,
|
total: number,
|
||||||
): NotificationProps & { id: string } => {
|
): NotificationData & { id: string } => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: header,
|
title: header,
|
||||||
|
@ -57,7 +57,7 @@ export const notification = {
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
end: (id: string, header: string): NotificationProps & { id: string } => {
|
end: (id: string, header: string): NotificationData & { id: string } => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: header,
|
title: header,
|
||||||
|
|
|
@ -52,7 +52,7 @@ const Authentication: FunctionComponent = () => {
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
></PasswordInput>
|
></PasswordInput>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Button fullWidth uppercase type="submit">
|
<Button fullWidth tt="uppercase" type="submit">
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { PageTable } from "@/components";
|
||||||
import MutateAction from "@/components/async/MutateAction";
|
import MutateAction from "@/components/async/MutateAction";
|
||||||
import Language from "@/components/bazarr/Language";
|
import Language from "@/components/bazarr/Language";
|
||||||
import TextPopover from "@/components/TextPopover";
|
import TextPopover from "@/components/TextPopover";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Anchor, Text } from "@mantine/core";
|
import { Anchor, Text } from "@mantine/core";
|
||||||
import { FunctionComponent, useMemo } from "react";
|
import { FunctionComponent, useMemo } from "react";
|
||||||
|
@ -22,9 +21,8 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
|
||||||
accessor: "title",
|
accessor: "title",
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const target = `/movies/${row.row.original.radarrId}`;
|
const target = `/movies/${row.row.original.radarrId}`;
|
||||||
const { classes } = useTableStyles();
|
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{row.value}
|
{row.value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { PageTable } from "@/components";
|
||||||
import MutateAction from "@/components/async/MutateAction";
|
import MutateAction from "@/components/async/MutateAction";
|
||||||
import Language from "@/components/bazarr/Language";
|
import Language from "@/components/bazarr/Language";
|
||||||
import TextPopover from "@/components/TextPopover";
|
import TextPopover from "@/components/TextPopover";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Anchor, Text } from "@mantine/core";
|
import { Anchor, Text } from "@mantine/core";
|
||||||
import { FunctionComponent, useMemo } from "react";
|
import { FunctionComponent, useMemo } from "react";
|
||||||
|
@ -21,10 +20,9 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
|
||||||
Header: "Series",
|
Header: "Series",
|
||||||
accessor: "seriesTitle",
|
accessor: "seriesTitle",
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const target = `/series/${row.row.original.sonarrSeriesId}`;
|
const target = `/series/${row.row.original.sonarrSeriesId}`;
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{row.value}
|
{row.value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
|
|
@ -125,7 +125,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
||||||
<DropContent></DropContent>
|
<DropContent></DropContent>
|
||||||
</Dropzone.FullScreen>
|
</Dropzone.FullScreen>
|
||||||
<Toolbox>
|
<Toolbox>
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Toolbox.Button
|
<Toolbox.Button
|
||||||
icon={faSync}
|
icon={faSync}
|
||||||
disabled={!available || hasTask}
|
disabled={!available || hasTask}
|
||||||
|
@ -160,7 +160,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
||||||
Search
|
Search
|
||||||
</Toolbox.Button>
|
</Toolbox.Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Toolbox.Button
|
<Toolbox.Button
|
||||||
disabled={
|
disabled={
|
||||||
series === undefined ||
|
series === undefined ||
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { AudioList } from "@/components/bazarr";
|
||||||
import { EpisodeHistoryModal } from "@/components/modals";
|
import { EpisodeHistoryModal } from "@/components/modals";
|
||||||
import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
|
import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
|
||||||
import { useModals } from "@/modules/modals";
|
import { useModals } from "@/modules/modals";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { BuildKey, filterSubtitleBy } from "@/utilities";
|
import { BuildKey, filterSubtitleBy } from "@/utilities";
|
||||||
import { useProfileItemsToLanguages } from "@/utilities/languages";
|
import { useProfileItemsToLanguages } from "@/utilities/languages";
|
||||||
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
@ -92,7 +91,7 @@ const Table: FunctionComponent<Props> = ({
|
||||||
{
|
{
|
||||||
accessor: "season",
|
accessor: "season",
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
return <Text>Season {row.value}</Text>;
|
return <Text span>Season {row.value}</Text>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -103,11 +102,9 @@ const Table: FunctionComponent<Props> = ({
|
||||||
Header: "Title",
|
Header: "Title",
|
||||||
accessor: "title",
|
accessor: "title",
|
||||||
Cell: ({ value, row }) => {
|
Cell: ({ value, row }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextPopover text={row.original.sceneName}>
|
<TextPopover text={row.original.sceneName}>
|
||||||
<Text className={classes.primary}>{value}</Text>
|
<Text className="table-primary">{value}</Text>
|
||||||
</TextPopover>
|
</TextPopover>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -156,7 +153,7 @@ const Table: FunctionComponent<Props> = ({
|
||||||
}, [episode, seriesId]);
|
}, [episode, seriesId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs" noWrap>
|
<Group gap="xs" wrap="nowrap">
|
||||||
{elements}
|
{elements}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
@ -168,7 +165,7 @@ const Table: FunctionComponent<Props> = ({
|
||||||
Cell: ({ row }) => {
|
Cell: ({ row }) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs" noWrap>
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Action
|
<Action
|
||||||
label="Manual Search"
|
label="Manual Search"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import Language from "@/components/bazarr/Language";
|
||||||
import StateIcon from "@/components/StateIcon";
|
import StateIcon from "@/components/StateIcon";
|
||||||
import TextPopover from "@/components/TextPopover";
|
import TextPopover from "@/components/TextPopover";
|
||||||
import HistoryView from "@/pages/views/HistoryView";
|
import HistoryView from "@/pages/views/HistoryView";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import {
|
import {
|
||||||
faFileExcel,
|
faFileExcel,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
|
@ -29,10 +28,9 @@ const MoviesHistoryView: FunctionComponent = () => {
|
||||||
Header: "Name",
|
Header: "Name",
|
||||||
accessor: "title",
|
accessor: "title",
|
||||||
Cell: ({ row, value }) => {
|
Cell: ({ row, value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const target = `/movies/${row.original.radarrId}`;
|
const target = `/movies/${row.original.radarrId}`;
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{value}
|
{value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,7 +9,6 @@ import Language from "@/components/bazarr/Language";
|
||||||
import StateIcon from "@/components/StateIcon";
|
import StateIcon from "@/components/StateIcon";
|
||||||
import TextPopover from "@/components/TextPopover";
|
import TextPopover from "@/components/TextPopover";
|
||||||
import HistoryView from "@/pages/views/HistoryView";
|
import HistoryView from "@/pages/views/HistoryView";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import {
|
import {
|
||||||
faFileExcel,
|
faFileExcel,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
|
@ -32,11 +31,10 @@ const SeriesHistoryView: FunctionComponent = () => {
|
||||||
Header: "Series",
|
Header: "Series",
|
||||||
accessor: "seriesTitle",
|
accessor: "seriesTitle",
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const target = `/series/${row.row.original.sonarrSeriesId}`;
|
const target = `/series/${row.row.original.sonarrSeriesId}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{row.value}
|
{row.value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
@ -50,8 +48,7 @@ const SeriesHistoryView: FunctionComponent = () => {
|
||||||
Header: "Title",
|
Header: "Title",
|
||||||
accessor: "episodeTitle",
|
accessor: "episodeTitle",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value}</Text>;
|
||||||
return <Text className={classes.noWrap}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - $header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 90%;
|
||||||
|
}
|
|
@ -5,16 +5,8 @@ import {
|
||||||
} from "@/apis/hooks";
|
} from "@/apis/hooks";
|
||||||
import { Selector, Toolbox } from "@/components";
|
import { Selector, Toolbox } from "@/components";
|
||||||
import { QueryOverlay } from "@/components/async";
|
import { QueryOverlay } from "@/components/async";
|
||||||
import Language from "@/components/bazarr/Language";
|
|
||||||
import { Layout } from "@/constants";
|
|
||||||
import { useSelectorOptions } from "@/utilities";
|
import { useSelectorOptions } from "@/utilities";
|
||||||
import {
|
import { Box, Container, SimpleGrid, useMantineTheme } from "@mantine/core";
|
||||||
Box,
|
|
||||||
Container,
|
|
||||||
SimpleGrid,
|
|
||||||
createStyles,
|
|
||||||
useMantineTheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useDocumentTitle } from "@mantine/hooks";
|
import { useDocumentTitle } from "@mantine/hooks";
|
||||||
import { merge } from "lodash";
|
import { merge } from "lodash";
|
||||||
import { FunctionComponent, useMemo, useState } from "react";
|
import { FunctionComponent, useMemo, useState } from "react";
|
||||||
|
@ -29,17 +21,7 @@ import {
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { actionOptions, timeFrameOptions } from "./options";
|
import { actionOptions, timeFrameOptions } from "./options";
|
||||||
|
import styles from "./HistoryStats.module.scss";
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
container: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
height: `calc(100vh - ${Layout.HEADER_HEIGHT}px)`,
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
height: "90%",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const HistoryStats: FunctionComponent = () => {
|
const HistoryStats: FunctionComponent = () => {
|
||||||
const { data: providers } = useSystemProviders(true);
|
const { data: providers } = useSystemProviders(true);
|
||||||
|
@ -71,8 +53,8 @@ const HistoryStats: FunctionComponent = () => {
|
||||||
date: v.date,
|
date: v.date,
|
||||||
series: v.count,
|
series: v.count,
|
||||||
}));
|
}));
|
||||||
const result = merge(movies, series);
|
|
||||||
return result;
|
return merge(movies, series);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -80,20 +62,13 @@ const HistoryStats: FunctionComponent = () => {
|
||||||
|
|
||||||
useDocumentTitle("History Statistics - Bazarr");
|
useDocumentTitle("History Statistics - Bazarr");
|
||||||
|
|
||||||
const { classes } = useStyles();
|
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid px={0} className={classes.container}>
|
<Container fluid px={0} className={styles.container}>
|
||||||
<QueryOverlay result={stats}>
|
<QueryOverlay result={stats}>
|
||||||
<Toolbox>
|
<Toolbox>
|
||||||
<SimpleGrid
|
<SimpleGrid cols={{ base: 4, xs: 2 }}>
|
||||||
cols={4}
|
|
||||||
breakpoints={[
|
|
||||||
{ maxWidth: "sm", cols: 4 },
|
|
||||||
{ maxWidth: "xs", cols: 2 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Selector
|
<Selector
|
||||||
placeholder="Time..."
|
placeholder="Time..."
|
||||||
options={timeFrameOptions}
|
options={timeFrameOptions}
|
||||||
|
@ -123,9 +98,9 @@ const HistoryStats: FunctionComponent = () => {
|
||||||
></Selector>
|
></Selector>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Toolbox>
|
</Toolbox>
|
||||||
<Box className={classes.chart} m="xs">
|
<Box className={styles.chart} m="xs">
|
||||||
<ResponsiveContainer>
|
<ResponsiveContainer>
|
||||||
<BarChart className={classes.chart} data={convertedData}>
|
<BarChart className={styles.chart} data={convertedData}>
|
||||||
<CartesianGrid strokeDasharray="4 2"></CartesianGrid>
|
<CartesianGrid strokeDasharray="4 2"></CartesianGrid>
|
||||||
<XAxis dataKey="date"></XAxis>
|
<XAxis dataKey="date"></XAxis>
|
||||||
<YAxis allowDecimals={false}></YAxis>
|
<YAxis allowDecimals={false}></YAxis>
|
|
@ -1,7 +1,7 @@
|
||||||
import { renderTest, RenderTestCase } from "@/tests/render";
|
import { renderTest, RenderTestCase } from "@/tests/render";
|
||||||
import MoviesHistoryView from "./Movies";
|
import MoviesHistoryView from "./Movies";
|
||||||
import SeriesHistoryView from "./Series";
|
import SeriesHistoryView from "./Series";
|
||||||
import HistoryStats from "./Statistics";
|
import HistoryStats from "./Statistics/HistoryStats";
|
||||||
|
|
||||||
const cases: RenderTestCase[] = [
|
const cases: RenderTestCase[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -123,7 +123,7 @@ const MovieDetailView: FunctionComponent = () => {
|
||||||
<DropContent></DropContent>
|
<DropContent></DropContent>
|
||||||
</Dropzone.FullScreen>
|
</Dropzone.FullScreen>
|
||||||
<Toolbox>
|
<Toolbox>
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Toolbox.Button
|
<Toolbox.Button
|
||||||
icon={faSync}
|
icon={faSync}
|
||||||
disabled={hasTask}
|
disabled={hasTask}
|
||||||
|
@ -168,7 +168,7 @@ const MovieDetailView: FunctionComponent = () => {
|
||||||
Manual
|
Manual
|
||||||
</Toolbox.Button>
|
</Toolbox.Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Toolbox.Button
|
<Toolbox.Button
|
||||||
disabled={!allowEdit || movie.profileId === null || hasTask}
|
disabled={!allowEdit || movie.profileId === null || hasTask}
|
||||||
icon={faCloudUploadAlt}
|
icon={faCloudUploadAlt}
|
||||||
|
@ -205,7 +205,7 @@ const MovieDetailView: FunctionComponent = () => {
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<FontAwesomeIcon icon={faToolbox} />}
|
leftSection={<FontAwesomeIcon icon={faToolbox} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (movie) {
|
if (movie) {
|
||||||
modals.openContextModal(SubtitleToolsModal, {
|
modals.openContextModal(SubtitleToolsModal, {
|
||||||
|
@ -217,7 +217,7 @@ const MovieDetailView: FunctionComponent = () => {
|
||||||
Mass Edit
|
Mass Edit
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<FontAwesomeIcon icon={faHistory} />}
|
leftSection={<FontAwesomeIcon icon={faHistory} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (movie) {
|
if (movie) {
|
||||||
modals.openContextModal(MovieHistoryModal, { movie });
|
modals.openContextModal(MovieHistoryModal, { movie });
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Action, SimpleTable } from "@/components";
|
||||||
import Language from "@/components/bazarr/Language";
|
import Language from "@/components/bazarr/Language";
|
||||||
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
|
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
|
||||||
import { task, TaskGroup } from "@/modules/task";
|
import { task, TaskGroup } from "@/modules/task";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { filterSubtitleBy } from "@/utilities";
|
import { filterSubtitleBy } from "@/utilities";
|
||||||
import { useProfileItemsToLanguages } from "@/utilities/languages";
|
import { useProfileItemsToLanguages } from "@/utilities/languages";
|
||||||
import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons";
|
import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
@ -40,17 +39,17 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
|
||||||
Header: "Subtitle Path",
|
Header: "Subtitle Path",
|
||||||
accessor: "path",
|
accessor: "path",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
|
|
||||||
const props: TextProps = {
|
const props: TextProps = {
|
||||||
className: classes.primary,
|
className: "table-primary",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSubtitleTrack(value)) {
|
if (isSubtitleTrack(value)) {
|
||||||
return <Text {...props}>Video File Subtitle Track</Text>;
|
return (
|
||||||
|
<Text className="table-primary">Video File Subtitle Track</Text>
|
||||||
|
);
|
||||||
} else if (isSubtitleMissing(value)) {
|
} else if (isSubtitleMissing(value)) {
|
||||||
return (
|
return (
|
||||||
<Text {...props} color="dimmed">
|
<Text {...props} c="dimmed">
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import LanguageProfileName from "@/components/bazarr/LanguageProfile";
|
||||||
import { ItemEditModal } from "@/components/forms/ItemEditForm";
|
import { ItemEditModal } from "@/components/forms/ItemEditForm";
|
||||||
import { useModals } from "@/modules/modals";
|
import { useModals } from "@/modules/modals";
|
||||||
import ItemView from "@/pages/views/ItemView";
|
import ItemView from "@/pages/views/ItemView";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { BuildKey } from "@/utilities";
|
import { BuildKey } from "@/utilities";
|
||||||
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
@ -35,10 +34,9 @@ const MovieView: FunctionComponent = () => {
|
||||||
Header: "Name",
|
Header: "Name",
|
||||||
accessor: "title",
|
accessor: "title",
|
||||||
Cell: ({ row, value }) => {
|
Cell: ({ row, value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const target = `/movies/${row.original.radarrId}`;
|
const target = `/movies/${row.original.radarrId}`;
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{value}
|
{value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,6 @@ import LanguageProfileName from "@/components/bazarr/LanguageProfile";
|
||||||
import { ItemEditModal } from "@/components/forms/ItemEditForm";
|
import { ItemEditModal } from "@/components/forms/ItemEditForm";
|
||||||
import { useModals } from "@/modules/modals";
|
import { useModals } from "@/modules/modals";
|
||||||
import ItemView from "@/pages/views/ItemView";
|
import ItemView from "@/pages/views/ItemView";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
@ -34,10 +33,9 @@ const SeriesView: FunctionComponent = () => {
|
||||||
Header: "Name",
|
Header: "Name",
|
||||||
accessor: "title",
|
accessor: "title",
|
||||||
Cell: ({ row, value }) => {
|
Cell: ({ row, value }) => {
|
||||||
const { classes } = useTableStyles();
|
|
||||||
const target = `/series/${row.original.sonarrSeriesId}`;
|
const target = `/series/${row.original.sonarrSeriesId}`;
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{value}
|
{value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
@ -70,13 +68,14 @@ const SeriesView: FunctionComponent = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Progress
|
<Progress.Root key={title} size="xl">
|
||||||
key={title}
|
<Progress.Section
|
||||||
size="xl"
|
value={progress}
|
||||||
color={episodeMissingCount === 0 ? "brand" : "yellow"}
|
color={episodeMissingCount === 0 ? "brand" : "yellow"}
|
||||||
value={progress}
|
>
|
||||||
label={label}
|
<Progress.Label>{label}</Progress.Label>
|
||||||
></Progress>
|
</Progress.Section>
|
||||||
|
</Progress.Root>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
faClipboard,
|
faClipboard,
|
||||||
faSync,
|
faSync,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Group as MantineGroup, Text as MantineText } from "@mantine/core";
|
import { Box, Group as MantineGroup, Text as MantineText } from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { FunctionComponent, useState } from "react";
|
import { FunctionComponent, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
@ -54,7 +54,7 @@ const SettingsGeneralView: FunctionComponent = () => {
|
||||||
></Number>
|
></Number>
|
||||||
<Text
|
<Text
|
||||||
label="Base URL"
|
label="Base URL"
|
||||||
icon="/"
|
leftSection="/"
|
||||||
settingKey="settings-general-base_url"
|
settingKey="settings-general-base_url"
|
||||||
settingOptions={{
|
settingOptions={{
|
||||||
onLoaded: (s) => s.general.base_url?.slice(1) ?? "",
|
onLoaded: (s) => s.general.base_url?.slice(1) ?? "",
|
||||||
|
@ -87,7 +87,7 @@ const SettingsGeneralView: FunctionComponent = () => {
|
||||||
rightSectionWidth={95}
|
rightSectionWidth={95}
|
||||||
rightSectionProps={{ style: { justifyContent: "flex-end" } }}
|
rightSectionProps={{ style: { justifyContent: "flex-end" } }}
|
||||||
rightSection={
|
rightSection={
|
||||||
<MantineGroup spacing="xs" mx="xs" position="right">
|
<MantineGroup gap="xs" mx="xs" justify="right">
|
||||||
{
|
{
|
||||||
// Clipboard API is only available in secure contexts See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#interfaces
|
// Clipboard API is only available in secure contexts See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#interfaces
|
||||||
window.isSecureContext && (
|
window.isSecureContext && (
|
||||||
|
@ -204,13 +204,12 @@ const SettingsGeneralView: FunctionComponent = () => {
|
||||||
<Number
|
<Number
|
||||||
label="Retention"
|
label="Retention"
|
||||||
settingKey="settings-backup-retention"
|
settingKey="settings-backup-retention"
|
||||||
styles={{
|
|
||||||
rightSection: { width: "4rem", justifyContent: "flex-end" },
|
|
||||||
}}
|
|
||||||
rightSection={
|
rightSection={
|
||||||
<MantineText size="xs" px="sm" color="dimmed">
|
<Box w="4rem" style={{ justifyContent: "flex-end" }}>
|
||||||
Days
|
<MantineText size="xs" px="sm" c="dimmed">
|
||||||
</MantineText>
|
Days
|
||||||
|
</MantineText>
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
></Number>
|
></Number>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
@ -355,7 +355,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SimpleTable data={equals} columns={columns}></SimpleTable>
|
<SimpleTable data={equals} columns={columns}></SimpleTable>
|
||||||
<Button fullWidth disabled={!canAdd} color="light" onClick={add}>
|
<Button fullWidth disabled={!canAdd} onClick={add}>
|
||||||
{canAdd ? "Add Equal" : "No Enabled Languages"}
|
{canAdd ? "Add Equal" : "No Enabled Languages"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -70,7 +70,7 @@ const Table: FunctionComponent = () => {
|
||||||
const items = row.value;
|
const items = row.value;
|
||||||
const cutoff = row.row.original.cutoff;
|
const cutoff = row.row.original.cutoff;
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs" noWrap>
|
<Group gap="xs" wrap="nowrap">
|
||||||
{items.map((v) => {
|
{items.map((v) => {
|
||||||
const isCutoff = v.id === cutoff || cutoff === anyCutoff;
|
const isCutoff = v.id === cutoff || cutoff === anyCutoff;
|
||||||
return (
|
return (
|
||||||
|
@ -128,7 +128,7 @@ const Table: FunctionComponent = () => {
|
||||||
Cell: ({ row }) => {
|
Cell: ({ row }) => {
|
||||||
const profile = row.original;
|
const profile = row.original;
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs" noWrap>
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Action
|
<Action
|
||||||
label="Edit Profile"
|
label="Edit Profile"
|
||||||
icon={faWrench}
|
icon={faWrench}
|
||||||
|
@ -163,7 +163,6 @@ const Table: FunctionComponent = () => {
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!canAdd}
|
disabled={!canAdd}
|
||||||
color="light"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const profile = {
|
const profile = {
|
||||||
profileId: nextProfileId,
|
profileId: nextProfileId,
|
||||||
|
|
|
@ -90,7 +90,7 @@ const NotificationForm: FunctionComponent<Props> = ({
|
||||||
></Textarea>
|
></Textarea>
|
||||||
</div>
|
</div>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Group position="right">
|
<Group justify="right">
|
||||||
<MutateButton mutation={test} args={() => form.values.url}>
|
<MutateButton mutation={test} args={() => form.values.url}>
|
||||||
Test
|
Test
|
||||||
</MutateButton>
|
</MutateButton>
|
||||||
|
|
|
@ -9,12 +9,12 @@ import {
|
||||||
Text as MantineText,
|
Text as MantineText,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
|
AutocompleteProps,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import {
|
import {
|
||||||
FunctionComponent,
|
FunctionComponent,
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
@ -50,6 +50,11 @@ interface ProviderViewProps {
|
||||||
settingsKey: SettingsKey;
|
settingsKey: SettingsKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProviderSelect {
|
||||||
|
value: string;
|
||||||
|
payload: ProviderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
export const ProviderView: FunctionComponent<ProviderViewProps> = ({
|
export const ProviderView: FunctionComponent<ProviderViewProps> = ({
|
||||||
availableOptions,
|
availableOptions,
|
||||||
settingsKey,
|
settingsKey,
|
||||||
|
@ -130,17 +135,16 @@ interface ProviderToolProps {
|
||||||
settingsKey: Readonly<SettingsKey>;
|
settingsKey: Readonly<SettingsKey>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectItem = forwardRef<
|
const SelectItem: AutocompleteProps["renderOption"] = ({ option }) => {
|
||||||
HTMLDivElement,
|
const provider = option as ProviderSelect;
|
||||||
{ payload: ProviderInfo; label: string }
|
|
||||||
>(({ payload: { description }, label, ...other }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1} ref={ref} {...other}>
|
<Stack gap={1}>
|
||||||
<MantineText size="md">{label}</MantineText>
|
<MantineText size="md">{provider.value}</MantineText>
|
||||||
<MantineText size="xs">{description}</MantineText>
|
<MantineText size="xs">{provider.payload.description}</MantineText>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
const ProviderTool: FunctionComponent<ProviderToolProps> = ({
|
const ProviderTool: FunctionComponent<ProviderToolProps> = ({
|
||||||
payload,
|
payload,
|
||||||
|
@ -298,19 +302,19 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Stack spacing="xs">{elements}</Stack>;
|
return <Stack gap="xs">{elements}</Stack>;
|
||||||
}, [info]);
|
}, [info]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsProvider value={settings}>
|
<SettingsProvider value={settings}>
|
||||||
<FormContext.Provider value={form}>
|
<FormContext.Provider value={form}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack spacing="xs">
|
<Stack gap="xs">
|
||||||
<Selector
|
<Selector
|
||||||
data-autofocus
|
data-autofocus
|
||||||
searchable
|
searchable
|
||||||
placeholder="Click to Select a Provider"
|
placeholder="Click to Select a Provider"
|
||||||
itemComponent={SelectItem}
|
renderOption={SelectItem}
|
||||||
disabled={payload !== null}
|
disabled={payload !== null}
|
||||||
{...selectorOptions}
|
{...selectorOptions}
|
||||||
value={info}
|
value={info}
|
||||||
|
@ -323,7 +327,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Group position="right">
|
<Group justify="right">
|
||||||
<Button hidden={!payload} color="red" onClick={deletePayload}>
|
<Button hidden={!payload} color="red" onClick={deletePayload}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -30,7 +30,7 @@ const SettingsRadarrView: FunctionComponent = () => {
|
||||||
<Number label="Port" settingKey="settings-radarr-port"></Number>
|
<Number label="Port" settingKey="settings-radarr-port"></Number>
|
||||||
<Text
|
<Text
|
||||||
label="Base URL"
|
label="Base URL"
|
||||||
icon="/"
|
leftSection="/"
|
||||||
settingKey="settings-radarr-base_url"
|
settingKey="settings-radarr-base_url"
|
||||||
settingOptions={{
|
settingOptions={{
|
||||||
onLoaded: (s) => s.radarr.base_url?.slice(1) ?? "",
|
onLoaded: (s) => s.radarr.base_url?.slice(1) ?? "",
|
||||||
|
|
|
@ -32,7 +32,7 @@ const SettingsSonarrView: FunctionComponent = () => {
|
||||||
<Number label="Port" settingKey="settings-sonarr-port"></Number>
|
<Number label="Port" settingKey="settings-sonarr-port"></Number>
|
||||||
<Text
|
<Text
|
||||||
label="Base URL"
|
label="Base URL"
|
||||||
icon="/"
|
leftSection="/"
|
||||||
settingKey="settings-sonarr-base_url"
|
settingKey="settings-sonarr-base_url"
|
||||||
settingOptions={{
|
settingOptions={{
|
||||||
onLoaded: (s) => s.sonarr.base_url?.slice(1) ?? "",
|
onLoaded: (s) => s.sonarr.base_url?.slice(1) ?? "",
|
||||||
|
|
|
@ -501,7 +501,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
||||||
label="Command"
|
label="Command"
|
||||||
settingKey="settings-general-postprocessing_cmd"
|
settingKey="settings-general-postprocessing_cmd"
|
||||||
></Text>
|
></Text>
|
||||||
<Table highlightOnHover fontSize="sm">
|
<Table highlightOnHover fs="sm">
|
||||||
<tbody>{commandOptionElements}</tbody>
|
<tbody>{commandOptionElements}</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</CollapseBox>
|
</CollapseBox>
|
||||||
|
|
9
frontend/src/pages/Settings/components/Card.module.scss
Normal file
9
frontend/src/pages/Settings/components/Card.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.card {
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
border: 1px solid var(--mantine-color-gray-7);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--mantine-shadow-md);
|
||||||
|
border: 1px solid $color-brand-5;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,8 @@
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
|
||||||
Center,
|
|
||||||
createStyles,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
UnstyledButton,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
|
import styles from "./Card.module.scss";
|
||||||
const useCardStyles = createStyles((theme) => {
|
|
||||||
return {
|
|
||||||
card: {
|
|
||||||
borderRadius: theme.radius.sm,
|
|
||||||
border: `1px solid ${theme.colors.gray[7]}`,
|
|
||||||
|
|
||||||
"&:hover": {
|
|
||||||
boxShadow: theme.shadows.md,
|
|
||||||
border: `1px solid ${theme.colors.brand[5]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stack: {
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
header?: string;
|
header?: string;
|
||||||
|
@ -39,16 +17,15 @@ export const Card: FunctionComponent<CardProps> = ({
|
||||||
plus,
|
plus,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { classes } = useCardStyles();
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton p="lg" onClick={onClick} className={classes.card}>
|
<UnstyledButton p="lg" onClick={onClick} className={styles.card}>
|
||||||
{plus ? (
|
{plus ? (
|
||||||
<Center>
|
<Center>
|
||||||
<FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon>
|
<FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon>
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
<Stack className={classes.stack} spacing={0} align="flex-start">
|
<Stack h="100%" gap={0} align="flex-start">
|
||||||
<Text weight="bold">{header}</Text>
|
<Text fw="bold">{header}</Text>
|
||||||
<Text hidden={description === undefined}>{description}</Text>
|
<Text hidden={description === undefined}>{description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -73,7 +73,7 @@ const Layout: FunctionComponent<Props> = (props) => {
|
||||||
icon={faSave}
|
icon={faSave}
|
||||||
loading={isMutating}
|
loading={isMutating}
|
||||||
disabled={totalStagedCount === 0}
|
disabled={totalStagedCount === 0}
|
||||||
rightIcon={
|
rightSection={
|
||||||
<Badge size="xs" radius="sm" hidden={totalStagedCount === 0}>
|
<Badge size="xs" radius="sm" hidden={totalStagedCount === 0}>
|
||||||
{totalStagedCount}
|
{totalStagedCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
|
@ -74,7 +74,7 @@ const LayoutModal: FunctionComponent<Props> = (props) => {
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Group position="right">
|
<Group justify="right">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={totalStagedCount === 0}
|
disabled={totalStagedCount === 0}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const Message: FunctionComponent<Props> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Text size="sm" color={type === "info" ? "dimmed" : "yellow"} my={0}>
|
<Text size="sm" c={type === "info" ? "dimmed" : "yellow"} my={0}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawRender, screen } from "@/tests";
|
import { render, screen } from "@/tests";
|
||||||
import { Text } from "@mantine/core";
|
import { Text } from "@mantine/core";
|
||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
@ -6,7 +6,7 @@ import { Section } from "./Section";
|
||||||
describe("Settings section", () => {
|
describe("Settings section", () => {
|
||||||
const header = "Section Header";
|
const header = "Section Header";
|
||||||
it("should show header", () => {
|
it("should show header", () => {
|
||||||
rawRender(<Section header="Section Header"></Section>);
|
render(<Section header="Section Header"></Section>);
|
||||||
|
|
||||||
expect(screen.getByText(header)).toBeDefined();
|
expect(screen.getByText(header)).toBeDefined();
|
||||||
expect(screen.getByRole("separator")).toBeDefined();
|
expect(screen.getByRole("separator")).toBeDefined();
|
||||||
|
@ -14,7 +14,7 @@ describe("Settings section", () => {
|
||||||
|
|
||||||
it("should show children", () => {
|
it("should show children", () => {
|
||||||
const text = "Section Child";
|
const text = "Section Child";
|
||||||
rawRender(
|
render(
|
||||||
<Section header="Section Header">
|
<Section header="Section Header">
|
||||||
<Text>{text}</Text>
|
<Text>{text}</Text>
|
||||||
</Section>,
|
</Section>,
|
||||||
|
@ -26,7 +26,7 @@ describe("Settings section", () => {
|
||||||
|
|
||||||
it("should work with hidden", () => {
|
it("should work with hidden", () => {
|
||||||
const text = "Section Child";
|
const text = "Section Child";
|
||||||
rawRender(
|
render(
|
||||||
<Section header="Section Header" hidden>
|
<Section header="Section Header" hidden>
|
||||||
<Text>{text}</Text>
|
<Text>{text}</Text>
|
||||||
</Section>,
|
</Section>,
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const Section: FunctionComponent<Props> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Stack hidden={hidden} spacing="xs" my="lg">
|
<Stack hidden={hidden} gap="xs" my="lg">
|
||||||
<Title order={4}>{header}</Title>
|
<Title order={4}>{header}</Title>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const CollapseBox: FunctionComponent<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse in={open} pl={indent ? "md" : undefined}>
|
<Collapse in={open} pl={indent ? "md" : undefined}>
|
||||||
<Stack spacing="xs">{children}</Stack>
|
<Stack gap="xs">{children}</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawRender, RenderOptions, screen } from "@/tests";
|
import { render, RenderOptions, screen } from "@/tests";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
|
import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
|
||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
|
@ -18,7 +18,7 @@ const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
||||||
const formRender = (
|
const formRender = (
|
||||||
ui: ReactElement,
|
ui: ReactElement,
|
||||||
options?: Omit<RenderOptions, "wrapper">,
|
options?: Omit<RenderOptions, "wrapper">,
|
||||||
) => rawRender(ui, { wrapper: FormSupport, ...options });
|
) => render(<FormSupport>{ui}</FormSupport>);
|
||||||
|
|
||||||
describe("Settings form", () => {
|
describe("Settings form", () => {
|
||||||
describe("number component", () => {
|
describe("number component", () => {
|
||||||
|
|
|
@ -38,6 +38,11 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
|
||||||
if (val === "") {
|
if (val === "") {
|
||||||
val = 0;
|
val = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return update(+val);
|
||||||
|
}
|
||||||
|
|
||||||
update(val);
|
update(val);
|
||||||
}}
|
}}
|
||||||
></NumberInput>
|
></NumberInput>
|
||||||
|
|
|
@ -56,7 +56,7 @@ export const URLTestButton: FunctionComponent<{
|
||||||
}, [address, port, url, apikey, ssl]);
|
}, [address, port, url, apikey, ssl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={click} color={color} title={title}>
|
<Button autoContrast onClick={click} variant={color} title={title}>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -107,7 +107,7 @@ export const ProviderTestButton: FunctionComponent<{
|
||||||
}, [testUrl]);
|
}, [testUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={click} color={color} title={title}>
|
<Button onClick={click} variant={color} title={title}>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -141,7 +141,7 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
></SimpleTable>
|
></SimpleTable>
|
||||||
<Button fullWidth color="light" onClick={addRow}>
|
<Button fullWidth onClick={addRow}>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks";
|
import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks";
|
||||||
import { SimpleTable } from "@/components";
|
import { SimpleTable } from "@/components";
|
||||||
import { MutateAction } from "@/components/async";
|
import { MutateAction } from "@/components/async";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
|
import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Anchor, Text } from "@mantine/core";
|
import { Anchor, Text } from "@mantine/core";
|
||||||
import { FunctionComponent, useMemo } from "react";
|
import { FunctionComponent, useMemo } from "react";
|
||||||
|
@ -20,16 +19,14 @@ const Table: FunctionComponent<Props> = ({ announcements }) => {
|
||||||
Header: "Since",
|
Header: "Since",
|
||||||
accessor: "timestamp",
|
accessor: "timestamp",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-primary">{value}</Text>;
|
||||||
return <Text className={classes.primary}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Announcement",
|
Header: "Announcement",
|
||||||
accessor: "text",
|
accessor: "text",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-primary">{value}</Text>;
|
||||||
return <Text className={classes.primary}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useDeleteBackups, useRestoreBackups } from "@/apis/hooks";
|
import { useDeleteBackups, useRestoreBackups } from "@/apis/hooks";
|
||||||
import { Action, PageTable } from "@/components";
|
import { Action, PageTable } from "@/components";
|
||||||
import { useModals } from "@/modules/modals";
|
import { useModals } from "@/modules/modals";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { Environment } from "@/utilities";
|
import { Environment } from "@/utilities";
|
||||||
import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Anchor, Text } from "@mantine/core";
|
import { Anchor, Text } from "@mantine/core";
|
||||||
|
@ -32,16 +31,14 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
|
||||||
Header: "Size",
|
Header: "Size",
|
||||||
accessor: "size",
|
accessor: "size",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value}</Text>;
|
||||||
return <Text className={classes.noWrap}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Time",
|
Header: "Time",
|
||||||
accessor: "date",
|
accessor: "date",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value}</Text>;
|
||||||
return <Text className={classes.noWrap}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -86,7 +86,7 @@ const SystemLogsView: FunctionComponent = () => {
|
||||||
<Container fluid px={0}>
|
<Container fluid px={0}>
|
||||||
<QueryOverlay result={logs}>
|
<QueryOverlay result={logs}>
|
||||||
<Toolbox>
|
<Toolbox>
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Toolbox.Button
|
<Toolbox.Button
|
||||||
loading={isFetching}
|
loading={isFetching}
|
||||||
icon={faSync}
|
icon={faSync}
|
||||||
|
@ -108,7 +108,7 @@ const SystemLogsView: FunctionComponent = () => {
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
icon={faFilter}
|
icon={faFilter}
|
||||||
onClick={openFilterModal}
|
onClick={openFilterModal}
|
||||||
rightIcon={
|
rightSection={
|
||||||
suffix() !== "" ? (
|
suffix() !== "" ? (
|
||||||
<Badge size="xs" radius="sm">
|
<Badge size="xs" radius="sm">
|
||||||
{suffix()}
|
{suffix()}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const SystemReleasesView: FunctionComponent = () => {
|
||||||
return (
|
return (
|
||||||
<Container size={600} py={12}>
|
<Container size={600} py={12}>
|
||||||
<QueryOverlay result={releases}>
|
<QueryOverlay result={releases}>
|
||||||
<Stack spacing="lg">
|
<Stack gap="lg">
|
||||||
{data?.map((v, idx) => (
|
{data?.map((v, idx) => (
|
||||||
<ReleaseCard key={BuildKey(idx, v.date)} {...v}></ReleaseCard>
|
<ReleaseCard key={BuildKey(idx, v.date)} {...v}></ReleaseCard>
|
||||||
))}
|
))}
|
||||||
|
@ -47,7 +47,7 @@ const ReleaseCard: FunctionComponent<ReleaseInfo> = ({
|
||||||
return (
|
return (
|
||||||
<Card shadow="md" p="lg">
|
<Card shadow="md" p="lg">
|
||||||
<Group>
|
<Group>
|
||||||
<Text weight="bold">{name}</Text>
|
<Text fw="bold">{name}</Text>
|
||||||
<Badge color="blue">{date}</Badge>
|
<Badge color="blue">{date}</Badge>
|
||||||
<Badge color={prerelease ? "yellow" : "green"}>
|
<Badge color={prerelease ? "yellow" : "green"}>
|
||||||
{prerelease ? "Development" : "Master"}
|
{prerelease ? "Development" : "Master"}
|
||||||
|
|
|
@ -46,7 +46,7 @@ function Row(props: InfoProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Grid columns={10}>
|
<Grid columns={10}>
|
||||||
<Grid.Col span={2}>
|
<Grid.Col span={2}>
|
||||||
<Text size="sm" align="right" weight="bold">
|
<Text size="sm" ta="right" fw="bold">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
@ -85,9 +85,12 @@ const InfoContainer: FunctionComponent<
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Divider
|
<Divider
|
||||||
labelProps={{ size: "medium", weight: "bold" }}
|
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label={title}
|
label={
|
||||||
|
<Text size="md" fw="bold">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
></Divider>
|
></Divider>
|
||||||
{children}
|
{children}
|
||||||
<Space />
|
<Space />
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { SimpleTable } from "@/components";
|
import { SimpleTable } from "@/components";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { Text } from "@mantine/core";
|
import { Text } from "@mantine/core";
|
||||||
import { FunctionComponent, useMemo } from "react";
|
import { FunctionComponent, useMemo } from "react";
|
||||||
import { Column } from "react-table";
|
import { Column } from "react-table";
|
||||||
|
@ -15,16 +14,14 @@ const Table: FunctionComponent<Props> = ({ health }) => {
|
||||||
Header: "Object",
|
Header: "Object",
|
||||||
accessor: "object",
|
accessor: "object",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value}</Text>;
|
||||||
return <Text className={classes.noWrap}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Issue",
|
Header: "Issue",
|
||||||
accessor: "issue",
|
accessor: "issue",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-primary">{value}</Text>;
|
||||||
return <Text className={classes.primary}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useRunTask } from "@/apis/hooks";
|
import { useRunTask } from "@/apis/hooks";
|
||||||
import { SimpleTable } from "@/components";
|
import { SimpleTable } from "@/components";
|
||||||
import MutateAction from "@/components/async/MutateAction";
|
import MutateAction from "@/components/async/MutateAction";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Text } from "@mantine/core";
|
import { Text } from "@mantine/core";
|
||||||
import { FunctionComponent, useMemo } from "react";
|
import { FunctionComponent, useMemo } from "react";
|
||||||
|
@ -18,16 +17,14 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
|
||||||
Header: "Name",
|
Header: "Name",
|
||||||
accessor: "name",
|
accessor: "name",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-primary">{value}</Text>;
|
||||||
return <Text className={classes.primary}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Interval",
|
Header: "Interval",
|
||||||
accessor: "interval",
|
accessor: "interval",
|
||||||
Cell: ({ value }) => {
|
Cell: ({ value }) => {
|
||||||
const { classes } = useTableStyles();
|
return <Text className="table-no-wrap">{value}</Text>;
|
||||||
return <Text className={classes.noWrap}>{value}</Text>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -39,7 +39,7 @@ const WantedMoviesView: FunctionComponent = () => {
|
||||||
const { download } = useMovieSubtitleModification();
|
const { download } = useMovieSubtitleModification();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group spacing="sm">
|
<Group gap="sm">
|
||||||
{value.map((item, idx) => (
|
{value.map((item, idx) => (
|
||||||
<Badge
|
<Badge
|
||||||
color={download.isLoading ? "gray" : undefined}
|
color={download.isLoading ? "gray" : undefined}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
import Language from "@/components/bazarr/Language";
|
import Language from "@/components/bazarr/Language";
|
||||||
import { TaskGroup, task } from "@/modules/task";
|
import { TaskGroup, task } from "@/modules/task";
|
||||||
import WantedView from "@/pages/views/WantedView";
|
import WantedView from "@/pages/views/WantedView";
|
||||||
import { useTableStyles } from "@/styles";
|
|
||||||
import { BuildKey } from "@/utilities";
|
import { BuildKey } from "@/utilities";
|
||||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
@ -23,9 +22,8 @@ const WantedSeriesView: FunctionComponent = () => {
|
||||||
accessor: "seriesTitle",
|
accessor: "seriesTitle",
|
||||||
Cell: (row) => {
|
Cell: (row) => {
|
||||||
const target = `/series/${row.row.original.sonarrSeriesId}`;
|
const target = `/series/${row.row.original.sonarrSeriesId}`;
|
||||||
const { classes } = useTableStyles();
|
|
||||||
return (
|
return (
|
||||||
<Anchor className={classes.primary} component={Link} to={target}>
|
<Anchor className="table-primary" component={Link} to={target}>
|
||||||
{row.value}
|
{row.value}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +47,7 @@ const WantedSeriesView: FunctionComponent = () => {
|
||||||
const { download } = useEpisodeSubtitleModification();
|
const { download } = useEpisodeSubtitleModification();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group spacing="sm">
|
<Group gap="sm">
|
||||||
{value.map((item, idx) => (
|
{value.map((item, idx) => (
|
||||||
<Badge
|
<Badge
|
||||||
color={download.isLoading ? "gray" : undefined}
|
color={download.isLoading ? "gray" : undefined}
|
||||||
|
|
|
@ -45,13 +45,11 @@ const UIError: FunctionComponent<Props> = ({ error }) => {
|
||||||
<Center my="xl">
|
<Center my="xl">
|
||||||
<Code>{stack}</Code>
|
<Code>{stack}</Code>
|
||||||
</Center>
|
</Center>
|
||||||
<Group position="center">
|
<Group justify="center">
|
||||||
<Anchor href={`${GithubRepoRoot}/issues/new/choose`} target="_blank">
|
<Anchor href={`${GithubRepoRoot}/issues/new/choose`} target="_blank">
|
||||||
<Button color="yellow">Report Issue</Button>
|
<Button color="yellow">Report Issue</Button>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
<Button onClick={Reload} color="light">
|
<Button onClick={Reload}>Reload Page</Button>
|
||||||
Reload Page
|
|
||||||
</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue