Compare commits

...

5 Commits

Author SHA1 Message Date
Anderson Shindy Oki 4b06385351
Merge 7f8b939ad8 into a2fee0e1e4 2024-04-23 06:11:31 +00:00
Anderson Oki 7f8b939ad8 fix format 2024-04-23 15:11:25 +09:00
Anderson Oki eb70f16495 fix series episode label 2024-04-23 15:09:06 +09:00
Anderson Oki e2e6e823f0 add todo 2024-04-23 14:25:36 +09:00
Anderson Oki 480589930d feat: upgrade mantine 2024-04-23 14:25:36 +09:00
96 changed files with 1037 additions and 1209 deletions

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,12 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@mantine/core": "^6.0.21", "@mantine/core": "^7.8.0",
"@mantine/dropzone": "^6.0.21", "@mantine/dropzone": "^7.8.0",
"@mantine/form": "^6.0.21", "@mantine/form": "^7.8.0",
"@mantine/hooks": "^6.0.21", "@mantine/hooks": "^7.8.0",
"@mantine/modals": "^6.0.21", "@mantine/modals": "^7.8.0",
"@mantine/notifications": "^6.0.21", "@mantine/notifications": "^7.8.0",
"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",
@ -54,6 +54,8 @@
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"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",

View 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",
},
},
},
};

View File

@ -0,0 +1,9 @@
.header {
@include light {
color: var(--mantine-color-gray-0);
}
@include dark {
color: var(--mantine-color-dark-0);
}
}

View File

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

View File

@ -0,0 +1,68 @@
.anchor {
text-decoration: none;
@include light {
border-color: var(--mantine-color-gray-5);
}
@include dark {
border-color: var(--mantine-color-dark-5);
}
&.active {
@include light {
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 {
@include light {
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 {
@include light {
background-color: var(--mantine-color-gray-2);
}
@include dark {
background-color: var(--mantine-color-dark-6);
}
}
.text {
display: inline-flex;
align-items: center;
width: 100%;
@include light {
color: var(--mantine-color-gray-8);
}
@include dark {
color: var(--mantine-color-gray-5);
}
}

View File

@ -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,18 @@ 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,
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 +33,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,7 +96,6 @@ 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 { colorScheme, toggleColorScheme } = useMantineColorScheme();
@ -111,23 +109,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 +121,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 +144,9 @@ const AppNavbar: FunctionComponent = () => {
></Action> ></Action>
</Anchor> </Anchor>
</Group> </Group>
</MantineNavbar.Section> </AppShell.Section>
</Selection.Provider> </Selection.Provider>
</MantineNavbar> </AppShell.Navbar>
); );
}; };
@ -186,7 +171,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 +184,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 +229,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 +246,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 +271,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 +283,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>
)} )}

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

View File

@ -0,0 +1,35 @@
import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css";
import { createTheme, MantineProvider } from "@mantine/core";
import { FunctionComponent, PropsWithChildren } from "react";
import ThemeLoader from "@/App/ThemeLoader";
const themeProvider = createTheme({
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
colors: {
brand: [
"#F8F0FC",
"#F3D9FA",
"#EEBEFA",
"#E599F7",
"#DA77F2",
"#CC5DE8",
"#BE4BDB",
"#AE3EC9",
"#9C36B5",
"#862E9C",
],
},
primaryColor: "brand",
});
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<MantineProvider theme={themeProvider} defaultColorScheme="auto">
<ThemeLoader />
{children}
</MantineProvider>
);
};
export default ThemeProvider;

View File

@ -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: Layout.NAVBAR_WIDTH,
navbar={<AppNavbar></AppNavbar>} breakpoint: Layout.MOBILE_BREAKPOINT,
collapsed: { mobile: !navbar },
}}
header={{ height: { base: Layout.HEADER_HEIGHT } }}
padding={0} padding={0}
fixed
> >
<Outlet></Outlet> <AppHeader></AppHeader>
<AppNavbar></AppNavbar>
<AppShell.Main>
<Outlet></Outlet>
</AppShell.Main>
</AppShell> </AppShell>
</OnlineProvider> </OnlineProvider>
</NavbarProvider> </NavbarProvider>

View File

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

View File

@ -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[] {

44
frontend/src/_bazarr.scss Normal file
View File

@ -0,0 +1,44 @@
$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;
@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;
}
}
}
button {
@include dark {
color: $color-brand-0 !important;
}
}

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

View File

@ -0,0 +1,9 @@
.result {
@include light {
color: var(--mantine-color-dark-8);
}
@include dark {
color: var(--mantine-color-gray-1);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
.content {
@include smaller-than($mantine-breakpoint-md) {
padding: 0;
}
}

View File

@ -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;
@ -157,12 +157,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) {
@ -255,13 +253,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
.container {
pointer-events: none;
min-height: 220px;
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
import { import {
ComboboxItem,
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 +14,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;
}; };
@ -84,7 +84,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 +93,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 +145,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],

View File

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

View File

@ -0,0 +1,9 @@
.container {
display: block;
max-width: 100%;
overflow-x: auto;
}
.table {
border-collapse: collapse;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
.group {
@include light {
color: var(--mantine-color-gray-3);
}
@include dark {
color: var(--mantine-color-dark-5);
}
}

View File

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

View File

@ -1,9 +1,7 @@
import { MantineNumberSize } from "@mantine/core";
export const GithubRepoRoot = "https://github.com/morpheus65535/bazarr"; export const GithubRepoRoot = "https://github.com/morpheus65535/bazarr";
export const Layout = { export const Layout = {
NAVBAR_WIDTH: 200, NAVBAR_WIDTH: 200,
HEADER_HEIGHT: 64, HEADER_HEIGHT: 64,
MOBILE_BREAKPOINT: "sm" as MantineNumberSize, MOBILE_BREAKPOINT: "sm",
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
.container {
display: flex;
flex-direction: column;
height: calc(100vh - $header-height);
}
.chart {
height: 90%;
}

View File

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

View File

@ -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[] = [
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,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>
@ -79,9 +79,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 />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,11 +28,9 @@ import {
HoverCard, HoverCard,
Image, Image,
List, List,
MediaQuery,
Stack, Stack,
Text, Text,
Title, Title,
createStyles,
} from "@mantine/core"; } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
@ -41,25 +39,9 @@ interface Props {
details?: { icon: IconDefinition; text: string }[]; details?: { icon: IconDefinition; text: string }[];
} }
const useStyles = createStyles((theme) => {
return {
poster: {
maxWidth: "250px",
},
col: {
maxWidth: "100%",
},
group: {
maxWidth: "100%",
},
};
});
const ItemOverview: FunctionComponent<Props> = (props) => { const ItemOverview: FunctionComponent<Props> = (props) => {
const { item, details } = props; const { item, details } = props;
const { classes } = useStyles();
const detailBadges = useMemo(() => { const detailBadges = useMemo(() => {
const badges: (JSX.Element | null)[] = []; const badges: (JSX.Element | null)[] = [];
@ -150,21 +132,19 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
flexWrap: "nowrap", flexWrap: "nowrap",
}} }}
> >
<MediaQuery smallerThan="sm" styles={{ display: "none" }}> <Grid.Col span={3} hiddenFrom="sm">
<Grid.Col span={3}> <Image
<Image src={item?.poster}
src={item?.poster} mx="auto"
mx="auto" maw="250px"
className={classes.poster} fallbackSrc="https://placehold.co/250x250?text=Placeholder"
withPlaceholder ></Image>
></Image> </Grid.Col>
</Grid.Col> <Grid.Col span={8} maw="100%">
</MediaQuery> <Stack align="flex-start" gap="xs" mx={6}>
<Grid.Col span={8} className={classes.col}> <Group align="flex-start" wrap="nowrap" maw="100%">
<Stack align="flex-start" spacing="xs" mx={6}>
<Group align="flex-start" noWrap className={classes.group}>
<Title my={0}> <Title my={0}>
<Text inherit color="white"> <Text inherit c="white">
<Box component="span" mr={12}> <Box component="span" mr={12}>
<FontAwesomeIcon <FontAwesomeIcon
title={item?.monitored ? "monitored" : "unmonitored"} title={item?.monitored ? "monitored" : "unmonitored"}
@ -176,10 +156,7 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
</Title> </Title>
<HoverCard position="bottom" withArrow> <HoverCard position="bottom" withArrow>
<HoverCard.Target> <HoverCard.Target>
<Text <Text hidden={item?.alternativeTitles.length === 0} c="white">
hidden={item?.alternativeTitles.length === 0}
color="white"
>
<FontAwesomeIcon icon={faClone} /> <FontAwesomeIcon icon={faClone} />
</Text> </Text>
</HoverCard.Target> </HoverCard.Target>
@ -192,16 +169,16 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
</HoverCard.Dropdown> </HoverCard.Dropdown>
</HoverCard> </HoverCard>
</Group> </Group>
<Group spacing="xs" className={classes.group}> <Group gap="xs" maw="100%">
{detailBadges} {detailBadges}
</Group> </Group>
<Group spacing="xs" className={classes.group}> <Group gap="xs" maw="100%">
{audioBadges} {audioBadges}
</Group> </Group>
<Group spacing="xs" className={classes.group}> <Group gap="xs" maw="100%">
{languageBadges} {languageBadges}
</Group> </Group>
<Text size="sm" color="white"> <Text size="sm" c="white">
{item?.overview} {item?.overview}
</Text> </Text>
</Stack> </Stack>

View File

@ -36,13 +36,8 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const profileOptions = useSelectorOptions(profiles ?? [], (v) => v.name); const profileOptions = useSelectorOptions(profiles ?? [], (v) => v.name);
const profileOptionsWithAction = useMemo< const profileOptionsWithAction = useMemo<SelectorOption<Language.Profile>[]>(
SelectorOption<Language.Profile | null>[] () => [...profileOptions.options],
>(
() => [
{ label: "Clear", value: null, group: "Action" },
...profileOptions.options,
],
[profileOptions.options], [profileOptions.options],
); );
@ -82,6 +77,7 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
}, },
[selections], [selections],
); );
return ( return (
<Container fluid px={0}> <Container fluid px={0}>
<Toolbox> <Toolbox>

View File

@ -1,4 +1,3 @@
import ThemeProvider from "@/App/theme";
import queryClient from "@/apis/queries"; import queryClient from "@/apis/queries";
import { ModalsProvider } from "@/modules/modals"; import { ModalsProvider } from "@/modules/modals";
import "@fontsource/roboto/300.css"; import "@fontsource/roboto/300.css";
@ -7,6 +6,7 @@ import { FunctionComponent, PropsWithChildren } from "react";
import { QueryClientProvider } from "react-query"; import { QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools"; import { ReactQueryDevtools } from "react-query/devtools";
import { Environment } from "./utilities"; import { Environment } from "./utilities";
import ThemeProvider from "@/App/ThemeProvider";
export const AllProviders: FunctionComponent<PropsWithChildren> = ({ export const AllProviders: FunctionComponent<PropsWithChildren> = ({
children, children,

View File

@ -1 +0,0 @@
export * from "./table";

View File

@ -1,19 +0,0 @@
import { createStyles } from "@mantine/core";
export const useTableStyles = createStyles((theme) => ({
primary: {
display: "inline-block",
[theme.fn.smallerThan("sm")]: {
minWidth: "12rem",
},
},
noWrap: {
whiteSpace: "nowrap",
},
select: {
display: "inline-block",
[theme.fn.smallerThan("sm")]: {
minWidth: "10rem",
},
},
}));

View File

@ -31,6 +31,16 @@ export default defineConfig(async ({ mode, command }) => {
enableBuild: false, enableBuild: false,
}), }),
], ],
css: {
preprocessorOptions: {
scss: {
additionalData: `
@import "./src/_mantine";
@import "./src/_bazarr";
`,
},
},
},
base: "./", base: "./",
resolve: { resolve: {
alias: { alias: {