Upgraded mantine to v7.x

pull/2516/head v1.4.4-beta.0
Anderson Shindy Oki 5 months ago committed by GitHub
parent bb8233b599
commit be8f2d6d18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

File diff suppressed because it is too large Load Diff

@ -13,12 +13,12 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@mantine/core": "^6.0.21", "@mantine/core": "^7.10.1",
"@mantine/dropzone": "^6.0.21", "@mantine/dropzone": "^7.10.1",
"@mantine/form": "^6.0.21", "@mantine/form": "^7.10.1",
"@mantine/hooks": "^6.0.21", "@mantine/hooks": "^7.10.1",
"@mantine/modals": "^6.0.21", "@mantine/modals": "^7.10.1",
"@mantine/notifications": "^6.0.21", "@mantine/notifications": "^7.10.1",
"axios": "^1.6.8", "axios": "^1.6.8",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -53,6 +53,8 @@
"husky": "^9.0.11", "husky": "^9.0.11",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"postcss-preset-mantine": "^1.14.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0", "pretty-quick": "^4.0.0",

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

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

@ -1,6 +1,5 @@
import { useSystem, useSystemSettings } from "@/apis/hooks"; import { useSystem, useSystemSettings } from "@/apis/hooks";
import { Action, Search } from "@/components"; import { Action, Search } from "@/components";
import { Layout } from "@/constants";
import { useNavbar } from "@/contexts/Navbar"; import { useNavbar } from "@/contexts/Navbar";
import { useIsOnline } from "@/contexts/Online"; import { useIsOnline } from "@/contexts/Online";
import { Environment, useGotoHomepage } from "@/utilities"; import { Environment, useGotoHomepage } from "@/utilities";
@ -12,27 +11,16 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Anchor, Anchor,
AppShell,
Avatar, Avatar,
Badge, Badge,
Burger, Burger,
Divider, Divider,
Group, Group,
Header,
MediaQuery,
Menu, Menu,
createStyles,
} from "@mantine/core"; } from "@mantine/core";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import styles from "./Header.module.scss";
const useStyles = createStyles((theme) => {
const headerBackgroundColor =
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[4];
return {
header: {
backgroundColor: headerBackgroundColor,
},
};
});
const AppHeader: FunctionComponent = () => { const AppHeader: FunctionComponent = () => {
const { data: settings } = useSystemSettings(); const { data: settings } = useSystemSettings();
@ -47,39 +35,28 @@ const AppHeader: FunctionComponent = () => {
const goHome = useGotoHomepage(); const goHome = useGotoHomepage();
const { classes } = useStyles();
return ( return (
<Header p="md" height={Layout.HEADER_HEIGHT} className={classes.header}> <AppShell.Header p="md" className={styles.header}>
<Group position="apart" noWrap> <Group justify="space-between" wrap="nowrap">
<Group noWrap> <Group wrap="nowrap">
<MediaQuery <Anchor onClick={goHome} visibleFrom="sm">
smallerThan={Layout.MOBILE_BREAKPOINT} <Avatar
styles={{ display: "none" }} alt="brand"
> size={32}
<Anchor onClick={goHome}> src={`${Environment.baseUrl}/images/logo64.png`}
<Avatar ></Avatar>
alt="brand" </Anchor>
size={32} <Burger
src={`${Environment.baseUrl}/images/logo64.png`} opened={showed}
></Avatar> onClick={() => show(!showed)}
</Anchor> size="sm"
</MediaQuery> hiddenFrom="sm"
<MediaQuery ></Burger>
largerThan={Layout.MOBILE_BREAKPOINT}
styles={{ display: "none" }}
>
<Burger
opened={showed}
onClick={() => show(!showed)}
size="sm"
></Burger>
</MediaQuery>
<Badge size="lg" radius="sm"> <Badge size="lg" radius="sm">
Bazarr Bazarr
</Badge> </Badge>
</Group> </Group>
<Group spacing="xs" position="right" noWrap> <Group gap="xs" justify="right" wrap="nowrap">
<Search></Search> <Search></Search>
<Menu> <Menu>
<Menu.Target> <Menu.Target>
@ -95,13 +72,13 @@ const AppHeader: FunctionComponent = () => {
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item <Menu.Item
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />} leftSection={<FontAwesomeIcon icon={faArrowRotateLeft} />}
onClick={() => restart()} onClick={() => restart()}
> >
Restart Restart
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<FontAwesomeIcon icon={faPowerOff} />} leftSection={<FontAwesomeIcon icon={faPowerOff} />}
onClick={() => shutdown()} onClick={() => shutdown()}
> >
Shutdown Shutdown
@ -114,7 +91,7 @@ const AppHeader: FunctionComponent = () => {
</Menu> </Menu>
</Group> </Group>
</Group> </Group>
</Header> </AppShell.Header>
); );
}; };

@ -0,0 +1,56 @@
.anchor {
border-color: var(--mantine-color-gray-5);
text-decoration: none;
@include dark {
border-color: var(--mantine-color-dark-5);
}
&.active {
border-left: 2px solid $color-brand-4;
background-color: var(--mantine-color-gray-1);
@include dark {
border-left: 2px solid $color-brand-8;
background-color: var(--mantine-color-dark-8);
}
}
&.hover {
background-color: var(--mantine-color-gray-0);
@include dark {
background-color: var(--mantine-color-dark-7);
}
}
}
.badge {
margin-left: auto;
text-decoration: none;
box-shadow: var(--mantine-shadow-xs);
}
.icon {
width: 1.4rem;
margin-right: var(--mantine-spacing-xs);
}
.nav {
background-color: var(--mantine-color-gray-2);
@include dark {
background-color: var(--mantine-color-dark-8);
}
}
.text {
display: inline-flex;
align-items: center;
width: 100%;
color: var(--mantine-color-gray-8);
@include dark {
color: var(--mantine-color-gray-5);
}
}

@ -1,5 +1,4 @@
import { Action } from "@/components"; import { Action } from "@/components";
import { Layout } from "@/constants";
import { useNavbar } from "@/contexts/Navbar"; import { useNavbar } from "@/contexts/Navbar";
import { useRouteItems } from "@/Router"; import { useRouteItems } from "@/Router";
import { CustomRouteObject, Route } from "@/Router/type"; import { CustomRouteObject, Route } from "@/Router/type";
@ -14,19 +13,19 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Anchor, Anchor,
AppShell,
Badge, Badge,
Collapse, Collapse,
createStyles,
Divider, Divider,
Group, Group,
Navbar as MantineNavbar,
Stack, Stack,
Text, Text,
useComputedColorScheme,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { useHover } from "@mantine/hooks"; import { useHover } from "@mantine/hooks";
import clsx from "clsx"; import clsx from "clsx";
import { import React, {
createContext, createContext,
FunctionComponent, FunctionComponent,
useContext, useContext,
@ -35,6 +34,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom"; import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom";
import styles from "./Navbar.module.scss";
const Selection = createContext<{ const Selection = createContext<{
selection: string | null; selection: string | null;
@ -97,11 +97,12 @@ function useIsActive(parent: string, route: RouteObject) {
} }
const AppNavbar: FunctionComponent = () => { const AppNavbar: FunctionComponent = () => {
const { showed } = useNavbar();
const [selection, select] = useState<string | null>(null); const [selection, select] = useState<string | null>(null);
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const computedColorScheme = useComputedColorScheme("light");
const dark = computedColorScheme === "dark";
const routes = useRouteItems(); const routes = useRouteItems();
@ -111,23 +112,10 @@ const AppNavbar: FunctionComponent = () => {
}, [pathname]); }, [pathname]);
return ( return (
<MantineNavbar <AppShell.Navbar p="xs" className={styles.nav}>
p="xs"
hiddenBreakpoint={Layout.MOBILE_BREAKPOINT}
hidden={!showed}
width={{ [Layout.MOBILE_BREAKPOINT]: Layout.NAVBAR_WIDTH }}
styles={(theme) => ({
root: {
backgroundColor:
theme.colorScheme === "light"
? theme.colors.gray[2]
: theme.colors.dark[6],
},
})}
>
<Selection.Provider value={{ selection, select }}> <Selection.Provider value={{ selection, select }}>
<MantineNavbar.Section grow> <AppShell.Section grow>
<Stack spacing={0}> <Stack gap={0}>
{routes.map((route, idx) => ( {routes.map((route, idx) => (
<RouteItem <RouteItem
key={BuildKey("nav", idx)} key={BuildKey("nav", idx)}
@ -136,10 +124,10 @@ const AppNavbar: FunctionComponent = () => {
></RouteItem> ></RouteItem>
))} ))}
</Stack> </Stack>
</MantineNavbar.Section> </AppShell.Section>
<Divider></Divider> <Divider></Divider>
<MantineNavbar.Section mt="xs"> <AppShell.Section mt="xs">
<Group spacing="xs"> <Group gap="xs">
<Action <Action
label="Change Theme" label="Change Theme"
color={dark ? "yellow" : "indigo"} color={dark ? "yellow" : "indigo"}
@ -159,9 +147,9 @@ const AppNavbar: FunctionComponent = () => {
></Action> ></Action>
</Anchor> </Anchor>
</Group> </Group>
</MantineNavbar.Section> </AppShell.Section>
</Selection.Provider> </Selection.Provider>
</MantineNavbar> </AppShell.Navbar>
); );
}; };
@ -186,7 +174,7 @@ const RouteItem: FunctionComponent<{
if (children !== undefined) { if (children !== undefined) {
const elements = ( const elements = (
<Stack spacing={0}> <Stack gap={0}>
{children.map((child, idx) => ( {children.map((child, idx) => (
<RouteItem <RouteItem
parent={link} parent={link}
@ -199,7 +187,7 @@ const RouteItem: FunctionComponent<{
if (name) { if (name) {
return ( return (
<Stack spacing={0}> <Stack gap={0}>
<NavbarItem <NavbarItem
primary primary
name={name} name={name}
@ -244,53 +232,6 @@ const RouteItem: FunctionComponent<{
} }
}; };
const useStyles = createStyles((theme) => {
const borderColor =
theme.colorScheme === "light" ? theme.colors.gray[5] : theme.colors.dark[4];
const activeBorderColor =
theme.colorScheme === "light"
? theme.colors.brand[4]
: theme.colors.brand[8];
const activeBackgroundColor =
theme.colorScheme === "light" ? theme.colors.gray[1] : theme.colors.dark[8];
const hoverBackgroundColor =
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[7];
const textColor =
theme.colorScheme === "light" ? theme.colors.gray[8] : theme.colors.gray[5];
return {
text: {
display: "inline-flex",
alignItems: "center",
width: "100%",
color: textColor,
},
anchor: {
textDecoration: "none",
borderLeft: `2px solid ${borderColor}`,
},
active: {
backgroundColor: activeBackgroundColor,
borderLeft: `2px solid ${activeBorderColor}`,
boxShadow: theme.shadows.xs,
},
hover: {
backgroundColor: hoverBackgroundColor,
},
icon: { width: "1.4rem", marginRight: theme.spacing.xs },
badge: {
marginLeft: "auto",
textDecoration: "none",
boxShadow: theme.shadows.xs,
color: textColor,
},
};
});
interface NavbarItemProps { interface NavbarItemProps {
name: string; name: string;
link: string; link: string;
@ -308,8 +249,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
onClick, onClick,
primary = false, primary = false,
}) => { }) => {
const { classes } = useStyles();
const { show } = useNavbar(); const { show } = useNavbar();
const { ref, hovered } = useHover(); const { ref, hovered } = useHover();
@ -335,9 +274,9 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
}} }}
className={({ isActive }) => className={({ isActive }) =>
clsx( clsx(
clsx(classes.anchor, { clsx(styles.anchor, {
[classes.active]: isActive, [styles.active]: isActive,
[classes.hover]: hovered, [styles.hover]: hovered,
}), }),
) )
} }
@ -347,18 +286,19 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
inline inline
p="xs" p="xs"
size="sm" size="sm"
weight={primary ? "bold" : "normal"} fw={primary ? "bold" : "normal"}
className={classes.text} className={styles.text}
span
> >
{icon && ( {icon && (
<FontAwesomeIcon <FontAwesomeIcon
className={classes.icon} className={styles.icon}
icon={icon} icon={icon}
></FontAwesomeIcon> ></FontAwesomeIcon>
)} )}
{name} {name}
{shouldHideBadge === false && ( {!shouldHideBadge && (
<Badge className={classes.badge} radius="xs"> <Badge className={styles.badge} radius="xs">
{badge} {badge}
</Badge> </Badge>
)} )}

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

@ -0,0 +1,61 @@
import {
ActionIcon,
AppShell,
Badge,
Button,
createTheme,
MantineProvider,
} from "@mantine/core";
import { FunctionComponent, PropsWithChildren } from "react";
import ThemeLoader from "@/App/ThemeLoader";
import "@mantine/core/styles.layer.css";
import "@mantine/notifications/styles.layer.css";
import styleVars from "@/assets/_variables.module.scss";
import buttonClasses from "@/assets/button.module.scss";
import actionIconClasses from "@/assets/action_icon.module.scss";
import appShellClasses from "@/assets/app_shell.module.scss";
import badgeClasses from "@/assets/badge.module.scss";
const themeProvider = createTheme({
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
colors: {
brand: [
styleVars.colorBrand0,
styleVars.colorBrand1,
styleVars.colorBrand2,
styleVars.colorBrand3,
styleVars.colorBrand4,
styleVars.colorBrand5,
styleVars.colorBrand6,
styleVars.colorBrand7,
styleVars.colorBrand8,
styleVars.colorBrand9,
],
},
primaryColor: "brand",
components: {
ActionIcon: ActionIcon.extend({
classNames: actionIconClasses,
}),
AppShell: AppShell.extend({
classNames: appShellClasses,
}),
Badge: Badge.extend({
classNames: badgeClasses,
}),
Button: Button.extend({
classNames: buttonClasses,
}),
},
});
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<MantineProvider theme={themeProvider} defaultColorScheme="auto">
<ThemeLoader />
{children}
</MantineProvider>
);
};
export default ThemeProvider;

@ -1,7 +1,6 @@
import AppNavbar from "@/App/Navbar"; import AppNavbar from "@/App/Navbar";
import { RouterNames } from "@/Router/RouterNames"; import { RouterNames } from "@/Router/RouterNames";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import { Layout } from "@/constants";
import NavbarProvider from "@/contexts/Navbar"; import NavbarProvider from "@/contexts/Navbar";
import OnlineProvider from "@/contexts/Online"; import OnlineProvider from "@/contexts/Online";
import { notification } from "@/modules/task"; import { notification } from "@/modules/task";
@ -13,6 +12,7 @@ import { showNotification } from "@mantine/notifications";
import { FunctionComponent, useEffect, useState } from "react"; import { FunctionComponent, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom"; import { Outlet, useNavigate } from "react-router-dom";
import AppHeader from "./Header"; import AppHeader from "./Header";
import styleVars from "@/assets/_variables.module.scss";
const App: FunctionComponent = () => { const App: FunctionComponent = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -55,13 +55,19 @@ const App: FunctionComponent = () => {
<NavbarProvider value={{ showed: navbar, show: setNavbar }}> <NavbarProvider value={{ showed: navbar, show: setNavbar }}>
<OnlineProvider value={{ online, setOnline }}> <OnlineProvider value={{ online, setOnline }}>
<AppShell <AppShell
navbarOffsetBreakpoint={Layout.MOBILE_BREAKPOINT} navbar={{
header={<AppHeader></AppHeader>} width: styleVars.navBarWidth,
navbar={<AppNavbar></AppNavbar>} breakpoint: "sm",
collapsed: { mobile: !navbar },
}}
header={{ height: { base: styleVars.headerHeight } }}
padding={0} padding={0}
fixed
> >
<Outlet></Outlet> <AppHeader></AppHeader>
<AppNavbar></AppNavbar>
<AppShell.Main>
<Outlet></Outlet>
</AppShell.Main>
</AppShell> </AppShell>
</OnlineProvider> </OnlineProvider>
</NavbarProvider> </NavbarProvider>

@ -1,87 +0,0 @@
import { useSystemSettings } from "@/apis/hooks";
import {
ColorScheme,
ColorSchemeProvider,
createEmotionCache,
MantineProvider,
MantineThemeOverride,
} from "@mantine/core";
import { useColorScheme } from "@mantine/hooks";
import {
FunctionComponent,
PropsWithChildren,
useCallback,
useEffect,
useState,
} from "react";
const theme: MantineThemeOverride = {
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
colors: {
brand: [
"#F8F0FC",
"#F3D9FA",
"#EEBEFA",
"#E599F7",
"#DA77F2",
"#CC5DE8",
"#BE4BDB",
"#AE3EC9",
"#9C36B5",
"#862E9C",
],
},
primaryColor: "brand",
};
function useAutoColorScheme() {
const settings = useSystemSettings();
const settingsColorScheme = settings.data?.general.theme;
let preferredColorScheme: ColorScheme = useColorScheme();
switch (settingsColorScheme) {
case "light":
preferredColorScheme = "light" as ColorScheme;
break;
case "dark":
preferredColorScheme = "dark" as ColorScheme;
break;
}
const [colorScheme, setColorScheme] = useState(preferredColorScheme);
// automatically switch dark/light theme
useEffect(() => {
setColorScheme(preferredColorScheme);
}, [preferredColorScheme]);
const toggleColorScheme = useCallback((value?: ColorScheme) => {
setColorScheme((scheme) => value || (scheme === "dark" ? "light" : "dark"));
}, []);
return { colorScheme, setColorScheme, toggleColorScheme };
}
const emotionCache = createEmotionCache({ key: "bazarr" });
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
const { colorScheme, toggleColorScheme } = useAutoColorScheme();
return (
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{ colorScheme, ...theme }}
emotionCache={emotionCache}
>
{children}
</MantineProvider>
</ColorSchemeProvider>
);
};
export default ThemeProvider;

@ -53,7 +53,9 @@ import Redirector from "./Redirector";
import { RouterNames } from "./RouterNames"; import { RouterNames } from "./RouterNames";
import { CustomRouteObject } from "./type"; import { CustomRouteObject } from "./type";
const HistoryStats = lazy(() => import("@/pages/History/Statistics")); const HistoryStats = lazy(
() => import("@/pages/History/Statistics/HistoryStats"),
);
const SystemStatusView = lazy(() => import("@/pages/System/Status")); const SystemStatusView = lazy(() => import("@/pages/System/Status"));
function useRoutes(): CustomRouteObject[] { function useRoutes(): CustomRouteObject[] {

@ -0,0 +1,40 @@
$color-brand-0: #f8f0fc;
$color-brand-1: #f3d9fa;
$color-brand-2: #eebefa;
$color-brand-3: #e599f7;
$color-brand-4: #da77f2;
$color-brand-5: #cc5de8;
$color-brand-6: #be4bdb;
$color-brand-7: #ae3ec9;
$color-brand-8: #9c36b5;
$color-brand-9: #862e9c;
$header-height: 64px;
:global {
.table-long-break {
overflow-wrap: anywhere;
}
.table-primary {
display: inline-block;
font-size: var(--mantine-font-size-sm);
@include smaller-than($mantine-breakpoint-sm) {
min-width: 12rem;
}
}
.table-no-wrap {
white-space: nowrap;
}
.table-select {
display: inline-block;
@include smaller-than($mantine-breakpoint-sm) {
min-width: 10rem;
}
}
}

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

@ -0,0 +1,18 @@
$navbar-width: 200;
:export {
colorBrand0: $color-brand-0;
colorBrand1: $color-brand-1;
colorBrand2: $color-brand-2;
colorBrand3: $color-brand-3;
colorBrand4: $color-brand-4;
colorBrand5: $color-brand-5;
colorBrand6: $color-brand-6;
colorBrand7: $color-brand-7;
colorBrand8: $color-brand-8;
colorBrand9: $color-brand-9;
headerHeight: $header-height;
navBarWidth: $navbar-width;
}

@ -0,0 +1,14 @@
@layer mantine {
.root {
&[data-variant="light"] {
color: var(--mantine-color-dark-0);
}
@include light {
&[data-variant="light"] {
background-color: var(--mantine-color-gray-1);
color: var(--mantine-color-dark-2);
}
}
}
}

@ -0,0 +1,5 @@
.main {
@include dark {
background-color: rgb(26, 27, 30);
}
}

@ -0,0 +1,8 @@
.root {
background-color: var(--mantine-color-grape-light);
@include light {
color: var(--mantine-color-dark-filled);
background-color: var(--mantine-color-grape-light);
}
}

@ -0,0 +1,12 @@
@layer mantine {
.root {
@include dark {
color: var(--mantine-color-dark-0);
}
&[data-variant="danger"] {
background-color: var(--mantine-color-red-9);
color: var(--mantine-color-red-0);
}
}
}

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

@ -5,11 +5,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Anchor, Anchor,
Autocomplete, Autocomplete,
createStyles, ComboboxItem,
SelectItemProps, OptionsFilter,
} from "@mantine/core"; } from "@mantine/core";
import { forwardRef, FunctionComponent, useMemo, useState } from "react"; import { FunctionComponent, useMemo, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styles from "./Search.module.scss";
type SearchResultItem = { type SearchResultItem = {
value: string; value: string;
@ -41,36 +42,35 @@ function useSearch(query: string) {
); );
} }
const useStyles = createStyles((theme) => { const optionsFilter: OptionsFilter = ({ options, search }) => {
return { const lowercaseSearch = search.toLowerCase();
result: { const trimmedSearch = search.trim();
color:
theme.colorScheme === "light"
? theme.colors.dark[8]
: theme.colors.gray[1],
},
};
});
type ResultCompProps = SelectItemProps & SearchResultItem;
const ResultComponent = forwardRef<HTMLDivElement, ResultCompProps>(
({ link, value }, ref) => {
const styles = useStyles();
return (options as ComboboxItem[]).filter((option) => {
return ( return (
<Anchor option.value.toLowerCase().includes(lowercaseSearch) ||
component={Link} option.value
to={link} .normalize("NFD")
underline={false} .replace(/[\u0300-\u036f]/g, "")
className={styles.classes.result} .toLowerCase()
p="sm" .includes(trimmedSearch)
>
{value}
</Anchor>
); );
}, });
); };
const ResultComponent = ({ name, link }: { name: string; link: string }) => {
return (
<Anchor
component={Link}
to={link}
underline="never"
className={styles.result}
p="sm"
>
{name}
</Anchor>
);
};
const Search: FunctionComponent = () => { const Search: FunctionComponent = () => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -79,22 +79,22 @@ const Search: FunctionComponent = () => {
return ( return (
<Autocomplete <Autocomplete
icon={<FontAwesomeIcon icon={faSearch} />} leftSection={<FontAwesomeIcon icon={faSearch} />}
itemComponent={ResultComponent} renderOption={(input) => (
<ResultComponent
name={input.option.value}
link={
results.find((a) => a.value === input.option.value)?.link || "/"
}
/>
)}
placeholder="Search" placeholder="Search"
size="sm" size="sm"
data={results} data={results}
value={query} value={query}
onChange={setQuery} onChange={setQuery}
onBlur={() => setQuery("")} onBlur={() => setQuery("")}
filter={(value, item) => filter={optionsFilter}
item.value.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.includes(value.trim())
}
></Autocomplete> ></Autocomplete>
); );
}; };

@ -31,7 +31,7 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
return <FontAwesomeIcon icon={faListCheck} />; return <FontAwesomeIcon icon={faListCheck} />;
} else { } else {
return ( return (
<Text color={hasIssues ? "yellow" : "green"}> <Text c={hasIssues ? "yellow" : "green"} span>
<FontAwesomeIcon <FontAwesomeIcon
icon={hasIssues ? faExclamationCircle : faCheckCircle} icon={hasIssues ? faExclamationCircle : faCheckCircle}
/> />
@ -48,9 +48,9 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
</Text> </Text>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<Group position="left" spacing="xl" noWrap grow> <Group justify="left" gap="xl" wrap="nowrap" grow>
<Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto"> <Stack align="flex-start" justify="flex-start" gap="xs" mb="auto">
<Text color="green"> <Text c="green">
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon> <FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
</Text> </Text>
<List> <List>
@ -59,8 +59,8 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
))} ))}
</List> </List>
</Stack> </Stack>
<Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto"> <Stack align="flex-start" justify="flex-start" gap="xs" mb="auto">
<Text color="yellow"> <Text c="yellow">
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon> <FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
</Text> </Text>
<List> <List>

@ -148,7 +148,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
<Menu.Item <Menu.Item
key={tool.key} key={tool.key}
disabled={disabledTools} disabled={disabledTools}
icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>} leftSection={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
onClick={() => { onClick={() => {
if (tool.modal) { if (tool.modal) {
modals.openContextModal(tool.modal, { selections }); modals.openContextModal(tool.modal, { selections });
@ -164,7 +164,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
<Menu.Label>Actions</Menu.Label> <Menu.Label>Actions</Menu.Label>
<Menu.Item <Menu.Item
disabled={selections.length !== 0 || onAction === undefined} disabled={selections.length !== 0 || onAction === undefined}
icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>} leftSection={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
onClick={() => { onClick={() => {
onAction?.("search"); onAction?.("search");
}} }}
@ -174,7 +174,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
<Menu.Item <Menu.Item
disabled={selections.length === 0 || onAction === undefined} disabled={selections.length === 0 || onAction === undefined}
color="red" color="red"
icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>} leftSection={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
onClick={() => { onClick={() => {
modals.openConfirmModal({ modals.openConfirmModal({
title: "The following subtitles will be deleted", title: "The following subtitles will be deleted",

@ -13,7 +13,7 @@ const AudioList: FunctionComponent<AudioListProps> = ({
...group ...group
}) => { }) => {
return ( return (
<Group spacing="xs" {...group}> <Group gap="xs" {...group}>
{audios.map((audio, idx) => ( {audios.map((audio, idx) => (
<Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}> <Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}>
{audio.name} {audio.name}

@ -1,4 +1,4 @@
import { rawRender, screen } from "@/tests"; import { render, screen } from "@/tests";
import { describe, it } from "vitest"; import { describe, it } from "vitest";
import { Language } from "."; import { Language } from ".";
@ -9,13 +9,13 @@ describe("Language text", () => {
}; };
it("should show short text", () => { it("should show short text", () => {
rawRender(<Language.Text value={testLanguage}></Language.Text>); render(<Language.Text value={testLanguage}></Language.Text>);
expect(screen.getByText(testLanguage.code2)).toBeDefined(); expect(screen.getByText(testLanguage.code2)).toBeDefined();
}); });
it("should show long text", () => { it("should show long text", () => {
rawRender(<Language.Text value={testLanguage} long></Language.Text>); render(<Language.Text value={testLanguage} long></Language.Text>);
expect(screen.getByText(testLanguage.name)).toBeDefined(); expect(screen.getByText(testLanguage.name)).toBeDefined();
}); });
@ -23,7 +23,7 @@ describe("Language text", () => {
const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true }; const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true };
it("should show short text with HI", () => { it("should show short text with HI", () => {
rawRender(<Language.Text value={testLanguageWithHi}></Language.Text>); render(<Language.Text value={testLanguageWithHi}></Language.Text>);
const expectedText = `${testLanguageWithHi.code2}:HI`; const expectedText = `${testLanguageWithHi.code2}:HI`;
@ -31,7 +31,7 @@ describe("Language text", () => {
}); });
it("should show long text with HI", () => { it("should show long text with HI", () => {
rawRender(<Language.Text value={testLanguageWithHi} long></Language.Text>); render(<Language.Text value={testLanguageWithHi} long></Language.Text>);
const expectedText = `${testLanguageWithHi.name} HI`; const expectedText = `${testLanguageWithHi.name} HI`;
@ -44,7 +44,7 @@ describe("Language text", () => {
}; };
it("should show short text with Forced", () => { it("should show short text with Forced", () => {
rawRender(<Language.Text value={testLanguageWithForced}></Language.Text>); render(<Language.Text value={testLanguageWithForced}></Language.Text>);
const expectedText = `${testLanguageWithHi.code2}:Forced`; const expectedText = `${testLanguageWithHi.code2}:Forced`;
@ -52,9 +52,7 @@ describe("Language text", () => {
}); });
it("should show long text with Forced", () => { it("should show long text with Forced", () => {
rawRender( render(<Language.Text value={testLanguageWithForced} long></Language.Text>);
<Language.Text value={testLanguageWithForced} long></Language.Text>,
);
const expectedText = `${testLanguageWithHi.name} Forced`; const expectedText = `${testLanguageWithHi.name} Forced`;
@ -75,7 +73,7 @@ describe("Language list", () => {
]; ];
it("should show all languages", () => { it("should show all languages", () => {
rawRender(<Language.List value={elements}></Language.List>); render(<Language.List value={elements}></Language.List>);
elements.forEach((value) => { elements.forEach((value) => {
expect(screen.getByText(value.name)).toBeDefined(); expect(screen.getByText(value.name)).toBeDefined();

@ -49,7 +49,7 @@ type LanguageListProps = {
const LanguageList: FunctionComponent<LanguageListProps> = ({ value }) => { const LanguageList: FunctionComponent<LanguageListProps> = ({ value }) => {
return ( return (
<Group spacing="xs"> <Group gap="xs">
{value.map((v) => ( {value.map((v) => (
<Badge key={BuildKey(v.code2, v.code2, v.hi)}>{v.name}</Badge> <Badge key={BuildKey(v.code2, v.code2, v.hi)}>{v.name}</Badge>
))} ))}

@ -55,15 +55,17 @@ const FrameRateForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
})} })}
> >
<Stack> <Stack>
<Group spacing="xs" grow> <Group gap="xs" grow>
<NumberInput <NumberInput
placeholder="From" placeholder="From"
precision={2} decimalScale={2}
fixedDecimalScale
{...form.getInputProps("from")} {...form.getInputProps("from")}
></NumberInput> ></NumberInput>
<NumberInput <NumberInput
placeholder="To" placeholder="To"
precision={2} decimalScale={2}
fixedDecimalScale
{...form.getInputProps("to")} {...form.getInputProps("to")}
></NumberInput> ></NumberInput>
</Group> </Group>

@ -80,7 +80,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
label="Languages Profile" label="Languages Profile"
></Selector> ></Selector>
<Divider></Divider> <Divider></Divider>
<Group position="right"> <Group justify="right">
<Button <Button
disabled={isOverlayVisible} disabled={isOverlayVisible}
onClick={() => { onClick={() => {

@ -1,7 +1,6 @@
import { useMovieSubtitleModification } from "@/apis/hooks"; import { useMovieSubtitleModification } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { TaskGroup, task } from "@/modules/task"; import { TaskGroup, task } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities"; import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form"; import FormUtils from "@/utilities/form";
import { import {
@ -19,7 +18,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Button, Button,
Checkbox, Checkbox,
createStyles,
Divider, Divider,
MantineColor, MantineColor,
Stack, Stack,
@ -79,21 +77,12 @@ interface Props {
onComplete?: () => void; onComplete?: () => void;
} }
const useStyles = createStyles((theme) => {
return {
wrapper: {
overflowWrap: "anywhere",
},
};
});
const MovieUploadForm: FunctionComponent<Props> = ({ const MovieUploadForm: FunctionComponent<Props> = ({
files, files,
movie, movie,
onComplete, onComplete,
}) => { }) => {
const modals = useModals(); const modals = useModals();
const { classes } = useStyles();
const profile = useLanguageProfileBy(movie.profileId); const profile = useLanguageProfileBy(movie.profileId);
@ -187,7 +176,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
return ( return (
<TextPopover text={value?.messages}> <TextPopover text={value?.messages}>
<Text color={color} inline> <Text c={color} inline>
<FontAwesomeIcon icon={icon}></FontAwesomeIcon> <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
</Text> </Text>
</TextPopover> </TextPopover>
@ -199,9 +188,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
id: "filename", id: "filename",
accessor: "file", accessor: "file",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-primary">{value.name}</Text>;
return <Text className={classes.primary}>{value.name}</Text>;
}, },
}, },
{ {
@ -236,11 +223,10 @@ const MovieUploadForm: FunctionComponent<Props> = ({
Header: "Language", Header: "Language",
accessor: "language", accessor: "language",
Cell: ({ row: { original, index }, value }) => { Cell: ({ row: { original, index }, value }) => {
const { classes } = useTableStyles();
return ( return (
<Selector <Selector
{...languageOptions} {...languageOptions}
className={classes.select} className="table-long-break"
value={value} value={value}
onChange={(item) => { onChange={(item) => {
action.mutate(index, { ...original, language: item }); action.mutate(index, { ...original, language: item });
@ -289,7 +275,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
modals.closeSelf(); modals.closeSelf();
})} })}
> >
<Stack className={classes.wrapper}> <Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable> <SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider> <Divider></Divider>
<Button type="submit">Upload</Button> <Button type="submit">Upload</Button>

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

@ -1,6 +1,5 @@
import { Action, Selector, SelectorOption, SimpleTable } from "@/components"; import { Action, Selector, SelectorOption, SimpleTable } from "@/components";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities"; import { useArrayAction, useSelectorOptions } from "@/utilities";
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
import FormUtils from "@/utilities/form"; import FormUtils from "@/utilities/form";
@ -19,6 +18,7 @@ import { useForm } from "@mantine/form";
import { FunctionComponent, useCallback, useMemo } from "react"; import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table"; import { Column } from "react-table";
import ChipInput from "../inputs/ChipInput"; import ChipInput from "../inputs/ChipInput";
import styles from "./ProfileEditForm.module.scss";
export const anyCutoff = 65535; export const anyCutoff = 65535;
@ -162,12 +162,10 @@ const ProfileEditForm: FunctionComponent<Props> = ({
[code], [code],
); );
const { classes } = useTableStyles();
return ( return (
<Selector <Selector
{...languageOptions} {...languageOptions}
className={classes.select} className="table-select"
value={language} value={language}
onChange={(value) => { onChange={(value) => {
if (value) { if (value) {
@ -260,13 +258,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
multiple multiple
chevronPosition="right" chevronPosition="right"
defaultValue={["Languages"]} defaultValue={["Languages"]}
styles={(theme) => ({ className={styles.content}
content: {
[theme.fn.smallerThan("md")]: {
padding: 0,
},
},
})}
> >
<Accordion.Item value="Languages"> <Accordion.Item value="Languages">
<Stack> <Stack>
@ -275,7 +267,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
columns={columns} columns={columns}
data={form.values.items} data={form.values.items}
></SimpleTable> ></SimpleTable>
<Button fullWidth color="light" onClick={addItem}> <Button fullWidth onClick={addItem}>
Add Language Add Language
</Button> </Button>
<Selector <Selector

@ -5,7 +5,6 @@ import {
} from "@/apis/hooks"; } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities"; import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form"; import FormUtils from "@/utilities/form";
import { import {
@ -23,7 +22,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Button, Button,
Checkbox, Checkbox,
createStyles,
Divider, Divider,
MantineColor, MantineColor,
Stack, Stack,
@ -86,21 +84,12 @@ interface Props {
onComplete?: VoidFunction; onComplete?: VoidFunction;
} }
const useStyles = createStyles((theme) => {
return {
wrapper: {
overflowWrap: "anywhere",
},
};
});
const SeriesUploadForm: FunctionComponent<Props> = ({ const SeriesUploadForm: FunctionComponent<Props> = ({
series, series,
files, files,
onComplete, onComplete,
}) => { }) => {
const modals = useModals(); const modals = useModals();
const { classes } = useStyles();
const episodes = useEpisodesBySeriesId(series.sonarrSeriesId); const episodes = useEpisodesBySeriesId(series.sonarrSeriesId);
const episodeOptions = useSelectorOptions( const episodeOptions = useSelectorOptions(
episodes.data ?? [], episodes.data ?? [],
@ -225,8 +214,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
id: "filename", id: "filename",
accessor: "file", accessor: "file",
Cell: ({ value: { name } }) => { Cell: ({ value: { name } }) => {
const { classes } = useTableStyles(); return <Text className="table-primary">{name}</Text>;
return <Text className={classes.primary}>{name}</Text>;
}, },
}, },
{ {
@ -283,11 +271,10 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
), ),
accessor: "language", accessor: "language",
Cell: ({ row: { original, index }, value }) => { Cell: ({ row: { original, index }, value }) => {
const { classes } = useTableStyles();
return ( return (
<Selector <Selector
{...languageOptions} {...languageOptions}
className={classes.select} className="table-select"
value={value} value={value}
onChange={(item) => { onChange={(item) => {
action.mutate(index, { ...original, language: item }); action.mutate(index, { ...original, language: item });
@ -301,12 +288,11 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
Header: "Episode", Header: "Episode",
accessor: "episode", accessor: "episode",
Cell: ({ value, row }) => { Cell: ({ value, row }) => {
const { classes } = useTableStyles();
return ( return (
<Selector <Selector
{...episodeOptions} {...episodeOptions}
searchable searchable
className={classes.select} className="table-select"
value={value} value={value}
onChange={(item) => { onChange={(item) => {
action.mutate(row.index, { ...row.original, episode: item }); action.mutate(row.index, { ...row.original, episode: item });
@ -368,7 +354,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
modals.closeSelf(); modals.closeSelf();
})} })}
> >
<Stack className={classes.wrapper}> <Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable> <SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider> <Divider></Divider>
<Button type="submit">Upload</Button> <Button type="submit">Upload</Button>

@ -14,10 +14,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core"; import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { Selector, SelectorOption } from "../inputs"; import { GroupedSelector, Selector } from "../inputs";
const TaskName = "Syncing Subtitle"; const TaskName = "Syncing Subtitle";
interface SelectOptions {
group: string;
items: { value: string; label: string }[];
}
function useReferencedSubtitles( function useReferencedSubtitles(
mediaType: "episode" | "movie", mediaType: "episode" | "movie",
mediaId: number, mediaId: number,
@ -37,15 +42,21 @@ function useReferencedSubtitles(
const mediaData = mediaType === "episode" ? episodeData : movieData; const mediaData = mediaType === "episode" ? episodeData : movieData;
const subtitles: { group: string; value: string; label: string }[] = []; const subtitles: SelectOptions[] = [];
if (!mediaData.data) { if (!mediaData.data) {
return []; return [];
} else { } else {
if (mediaData.data.audio_tracks.length > 0) { if (mediaData.data.audio_tracks.length > 0) {
const embeddedAudioGroup: SelectOptions = {
group: "Embedded audio tracks",
items: [],
};
subtitles.push(embeddedAudioGroup);
mediaData.data.audio_tracks.forEach((item) => { mediaData.data.audio_tracks.forEach((item) => {
subtitles.push({ embeddedAudioGroup.items.push({
group: "Embedded audio tracks",
value: item.stream, value: item.stream,
label: `${item.name || item.language} (${item.stream})`, label: `${item.name || item.language} (${item.stream})`,
}); });
@ -53,9 +64,15 @@ function useReferencedSubtitles(
} }
if (mediaData.data.embedded_subtitles_tracks.length > 0) { if (mediaData.data.embedded_subtitles_tracks.length > 0) {
const embeddedSubtitlesTrackGroup: SelectOptions = {
group: "Embedded subtitles tracks",
items: [],
};
subtitles.push(embeddedSubtitlesTrackGroup);
mediaData.data.embedded_subtitles_tracks.forEach((item) => { mediaData.data.embedded_subtitles_tracks.forEach((item) => {
subtitles.push({ embeddedSubtitlesTrackGroup.items.push({
group: "Embedded subtitles tracks",
value: item.stream, value: item.stream,
label: `${item.name || item.language} (${item.stream})`, label: `${item.name || item.language} (${item.stream})`,
}); });
@ -63,10 +80,16 @@ function useReferencedSubtitles(
} }
if (mediaData.data.external_subtitles_tracks.length > 0) { if (mediaData.data.external_subtitles_tracks.length > 0) {
const externalSubtitlesFilesGroup: SelectOptions = {
group: "External Subtitles files",
items: [],
};
subtitles.push(externalSubtitlesFilesGroup);
mediaData.data.external_subtitles_tracks.forEach((item) => { mediaData.data.external_subtitles_tracks.forEach((item) => {
if (item) { if (item) {
subtitles.push({ externalSubtitlesFilesGroup.items.push({
group: "External Subtitles files",
value: item.path, value: item.path,
label: item.name, label: item.name,
}); });
@ -105,7 +128,7 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
const mediaId = selections[0].id; const mediaId = selections[0].id;
const subtitlesPath = selections[0].path; const subtitlesPath = selections[0].path;
const subtitles: SelectorOption<string>[] = useReferencedSubtitles( const subtitles: SelectOptions[] = useReferencedSubtitles(
mediaType, mediaType,
mediaId, mediaId,
subtitlesPath, subtitlesPath,
@ -145,14 +168,14 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
> >
<Text size="sm">{selections.length} subtitles selected</Text> <Text size="sm">{selections.length} subtitles selected</Text>
</Alert> </Alert>
<Selector <GroupedSelector
clearable clearable
disabled={subtitles.length === 0 || selections.length !== 1} disabled={subtitles.length === 0 || selections.length !== 1}
label="Reference" label="Reference"
placeholder="Default: choose automatically within video file" placeholder="Default: choose automatically within video file"
options={subtitles} options={subtitles}
{...form.getInputProps("reference")} {...form.getInputProps("reference")}
></Selector> ></GroupedSelector>
<Selector <Selector
clearable clearable
label="Max Offset Seconds" label="Max Offset Seconds"

@ -70,7 +70,7 @@ const TimeOffsetForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
})} })}
> >
<Stack> <Stack>
<Group align="end" spacing="xs" noWrap> <Group align="end" gap="xs" wrap="nowrap">
<Button <Button
color="gray" color="gray"
variant="filled" variant="filled"

@ -1,4 +1,4 @@
export { default as Search } from "./Search"; export { default as Search } from "./Search";
export * from "./inputs"; export * from "./inputs";
export * from "./tables"; export * from "./tables";
export { default as Toolbox } from "./toolbox"; export { default as Toolbox } from "./toolbox/Toolbox";

@ -1,4 +1,4 @@
import { rawRender, screen } from "@/tests"; import { render, screen } from "@/tests";
import { faStickyNote } from "@fortawesome/free-regular-svg-icons"; import { faStickyNote } from "@fortawesome/free-regular-svg-icons";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest"; import { describe, it, vitest } from "vitest";
@ -9,7 +9,7 @@ const testIcon = faStickyNote;
describe("Action button", () => { describe("Action button", () => {
it("should be a button", () => { it("should be a button", () => {
rawRender(<Action icon={testIcon} label={testLabel}></Action>); render(<Action icon={testIcon} label={testLabel}></Action>);
const element = screen.getByRole("button", { name: testLabel }); const element = screen.getByRole("button", { name: testLabel });
expect(element.getAttribute("type")).toEqual("button"); expect(element.getAttribute("type")).toEqual("button");
@ -17,7 +17,7 @@ describe("Action button", () => {
}); });
it("should show icon", () => { it("should show icon", () => {
rawRender(<Action icon={testIcon} label={testLabel}></Action>); render(<Action icon={testIcon} label={testLabel}></Action>);
// TODO: use getBy... // TODO: use getBy...
const element = screen.getByRole("img", { hidden: true }); const element = screen.getByRole("img", { hidden: true });
@ -27,7 +27,7 @@ describe("Action button", () => {
it("should call on-click event when clicked", async () => { it("should call on-click event when clicked", async () => {
const onClickFn = vitest.fn(); const onClickFn = vitest.fn();
rawRender( render(
<Action icon={testIcon} label={testLabel} onClick={onClickFn}></Action>, <Action icon={testIcon} label={testLabel} onClick={onClickFn}></Action>,
); );

@ -1,4 +1,4 @@
import { rawRender, screen } from "@/tests"; import { render, screen } from "@/tests";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest"; import { describe, it, vitest } from "vitest";
import ChipInput from "./ChipInput"; import ChipInput from "./ChipInput";
@ -8,7 +8,7 @@ describe("ChipInput", () => {
// TODO: Support default value // TODO: Support default value
it.skip("should works with default value", () => { it.skip("should works with default value", () => {
rawRender(<ChipInput defaultValue={existedValues}></ChipInput>); render(<ChipInput defaultValue={existedValues}></ChipInput>);
existedValues.forEach((value) => { existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined(); expect(screen.getByText(value)).toBeDefined();
@ -16,7 +16,7 @@ describe("ChipInput", () => {
}); });
it("should works with value", () => { it("should works with value", () => {
rawRender(<ChipInput value={existedValues}></ChipInput>); render(<ChipInput value={existedValues}></ChipInput>);
existedValues.forEach((value) => { existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined(); expect(screen.getByText(value)).toBeDefined();
@ -29,9 +29,7 @@ describe("ChipInput", () => {
expect(values).toContain(typedValue); expect(values).toContain(typedValue);
}); });
rawRender( render(<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>);
<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>,
);
const element = screen.getByRole("searchbox"); const element = screen.getByRole("searchbox");

@ -1,35 +1,29 @@
import { useSelectorOptions } from "@/utilities";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { MultiSelector, MultiSelectorProps } from "./Selector"; import { TagsInput } from "@mantine/core";
export type ChipInputProps = Omit< export interface ChipInputProps {
MultiSelectorProps<string>, defaultValue?: string[] | undefined;
| "searchable" value?: readonly string[] | null;
| "creatable" label?: string;
| "getCreateLabel" onChange?: (value: string[]) => void;
| "onCreate" }
| "options"
| "getkey"
>;
const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
const { value, onChange } = props;
const options = useSelectorOptions(value ?? [], (v) => v);
const ChipInput: FunctionComponent<ChipInputProps> = ({
defaultValue,
value,
label,
onChange,
}: ChipInputProps) => {
// TODO: Replace with our own custom implementation instead of just using the
// built-in TagsInput. https://mantine.dev/combobox/?e=MultiSelectCreatable
return ( return (
<MultiSelector <TagsInput
{...props} defaultValue={defaultValue}
{...options} label={label}
creatable value={value ? value?.map((v) => v) : []}
searchable onChange={onChange}
getCreateLabel={(query) => `Add "${query}"`} clearable
onCreate={(query) => { ></TagsInput>
onChange?.([...(value ?? []), query]);
return query;
}}
buildOption={(value) => value}
></MultiSelector>
); );
}; };

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

@ -4,24 +4,14 @@ import {
faXmark, faXmark,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Group, Stack, Text, createStyles } from "@mantine/core"; import { Group, Stack, Text } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import styles from "./DropContent.module.scss";
const useStyle = createStyles((theme) => {
return {
container: {
pointerEvents: "none",
minHeight: 220,
},
};
});
export const DropContent: FunctionComponent = () => { export const DropContent: FunctionComponent = () => {
const { classes } = useStyle();
return ( return (
<Group position="center" spacing="xl" className={classes.container}> <Group justify="center" gap="xl" className={styles.container}>
<Dropzone.Idle> <Dropzone.Idle>
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" /> <FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
</Dropzone.Idle> </Dropzone.Idle>
@ -31,9 +21,9 @@ export const DropContent: FunctionComponent = () => {
<Dropzone.Reject> <Dropzone.Reject>
<FontAwesomeIcon icon={faXmark} size="2x" /> <FontAwesomeIcon icon={faXmark} size="2x" />
</Dropzone.Reject> </Dropzone.Reject>
<Stack spacing={0}> <Stack gap={0}>
<Text size="lg">Upload Subtitles</Text> <Text size="lg">Upload Subtitles</Text>
<Text color="dimmed" size="sm"> <Text c="dimmed" size="sm">
Attach as many files as you like, you will need to select file Attach as many files as you like, you will need to select file
metadata before uploading metadata before uploading
</Text> </Text>

@ -1,7 +1,12 @@
import { useFileSystem } from "@/apis/hooks"; import { useFileSystem } from "@/apis/hooks";
import { faFolder } from "@fortawesome/free-regular-svg-icons"; import { faFolder } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Autocomplete, AutocompleteProps } from "@mantine/core"; import {
Autocomplete,
AutocompleteProps,
ComboboxItem,
OptionsFilter,
} from "@mantine/core";
import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react"; import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react";
// TODO: use fortawesome icons // TODO: use fortawesome icons
@ -75,24 +80,28 @@ export const FileBrowser: FunctionComponent<FileBrowserProps> = ({
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const optionsFilter: OptionsFilter = ({ options, search }) => {
return (options as ComboboxItem[]).filter((option) => {
if (search === backKey) {
return true;
}
return option.value.includes(search);
});
};
return ( return (
<Autocomplete <Autocomplete
{...props} {...props}
ref={ref} ref={ref}
icon={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>} leftSection={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
placeholder="Click to start" placeholder="Click to start"
data={data} data={data}
value={value} value={value}
// Temporary solution of infinite dropdown items, fix later // Temporary solution of infinite dropdown items, fix later
limit={NaN} limit={NaN}
maxDropdownHeight={240} maxDropdownHeight={240}
filter={(value, item) => { filter={optionsFilter}
if (item.value === backKey) {
return true;
} else {
return item.value.includes(value);
}
}}
onChange={(val) => { onChange={(val) => {
if (val !== backKey) { if (val !== backKey) {
setValue(val); setValue(val);

@ -1,4 +1,4 @@
import { rawRender, screen } from "@/tests"; import { render, screen } from "@/tests";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest"; import { describe, it, vitest } from "vitest";
import { Selector, SelectorOption } from "./Selector"; import { Selector, SelectorOption } from "./Selector";
@ -18,20 +18,17 @@ const testOptions: SelectorOption<string>[] = [
describe("Selector", () => { describe("Selector", () => {
describe("options", () => { describe("options", () => {
it("should work with the SelectorOption", () => { it("should work with the SelectorOption", () => {
rawRender( render(<Selector name={selectorName} options={testOptions}></Selector>);
<Selector name={selectorName} options={testOptions}></Selector>,
);
// TODO: selectorName testOptions.forEach((o) => {
expect(screen.getByRole("searchbox")).toBeDefined(); expect(screen.getByText(o.label)).toBeDefined();
});
}); });
it("should display when clicked", async () => { it("should display when clicked", async () => {
rawRender( render(<Selector name={selectorName} options={testOptions}></Selector>);
<Selector name={selectorName} options={testOptions}></Selector>,
);
const element = screen.getByRole("searchbox"); const element = screen.getByTestId("input-selector");
await userEvent.click(element); await userEvent.click(element);
@ -44,7 +41,7 @@ describe("Selector", () => {
it("shouldn't show default value", async () => { it("shouldn't show default value", async () => {
const option = testOptions[0]; const option = testOptions[0];
rawRender( render(
<Selector <Selector
name={selectorName} name={selectorName}
options={testOptions} options={testOptions}
@ -57,7 +54,7 @@ describe("Selector", () => {
it("shouldn't show value", async () => { it("shouldn't show value", async () => {
const option = testOptions[0]; const option = testOptions[0];
rawRender( render(
<Selector <Selector
name={selectorName} name={selectorName}
options={testOptions} options={testOptions}
@ -75,7 +72,7 @@ describe("Selector", () => {
const mockedFn = vitest.fn((value: string | null) => { const mockedFn = vitest.fn((value: string | null) => {
expect(value).toEqual(clickedOption.value); expect(value).toEqual(clickedOption.value);
}); });
rawRender( render(
<Selector <Selector
name={selectorName} name={selectorName}
options={testOptions} options={testOptions}
@ -83,13 +80,13 @@ describe("Selector", () => {
></Selector>, ></Selector>,
); );
const element = screen.getByRole("searchbox"); const element = screen.getByTestId("input-selector");
await userEvent.click(element); await userEvent.click(element);
await userEvent.click(screen.getByText(clickedOption.label)); await userEvent.click(screen.getByText(clickedOption.label));
expect(mockedFn).toBeCalled(); expect(mockedFn).toHaveBeenCalled();
}); });
}); });
@ -115,7 +112,7 @@ describe("Selector", () => {
const mockedFn = vitest.fn((value: { name: string } | null) => { const mockedFn = vitest.fn((value: { name: string } | null) => {
expect(value).toEqual(clickedOption.value); expect(value).toEqual(clickedOption.value);
}); });
rawRender( render(
<Selector <Selector
name={selectorName} name={selectorName}
options={objectOptions} options={objectOptions}
@ -124,20 +121,20 @@ describe("Selector", () => {
></Selector>, ></Selector>,
); );
const element = screen.getByRole("searchbox"); const element = screen.getByTestId("input-selector");
await userEvent.click(element); await userEvent.click(element);
await userEvent.click(screen.getByText(clickedOption.label)); await userEvent.click(screen.getByText(clickedOption.label));
expect(mockedFn).toBeCalled(); expect(mockedFn).toHaveBeenCalled();
}); });
}); });
describe("placeholder", () => { describe("placeholder", () => {
it("should show when no selection", () => { it("should show when no selection", () => {
const placeholder = "Empty Selection"; const placeholder = "Empty Selection";
rawRender( render(
<Selector <Selector
name={selectorName} name={selectorName}
options={testOptions} options={testOptions}

@ -1,9 +1,10 @@
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
import { import {
ComboboxItem,
ComboboxParsedItemGroup,
MultiSelect, MultiSelect,
MultiSelectProps, MultiSelectProps,
Select, Select,
SelectItem,
SelectProps, SelectProps,
} from "@mantine/core"; } from "@mantine/core";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
@ -14,10 +15,10 @@ export type SelectorOption<T> = Override<
value: T; value: T;
label: string; label: string;
}, },
SelectItem ComboboxItem
>; >;
type SelectItemWithPayload<T> = SelectItem & { type SelectItemWithPayload<T> = ComboboxItem & {
payload: T; payload: T;
}; };
@ -34,6 +35,30 @@ function DefaultKeyBuilder<T>(value: T) {
} }
} }
export type GroupedSelectorProps<T> = Override<
{
options: ComboboxParsedItemGroup[];
getkey?: (value: T) => string;
},
Omit<SelectProps, "data">
>;
export function GroupedSelector<T>({
value,
options,
getkey = DefaultKeyBuilder,
...select
}: GroupedSelectorProps<T>) {
return (
<Select
data-testid="input-selector"
comboboxProps={{ withinPortal: true }}
data={options}
{...select}
></Select>
);
}
export type SelectorProps<T> = Override< export type SelectorProps<T> = Override<
{ {
value?: T | null; value?: T | null;
@ -84,7 +109,7 @@ export function Selector<T>({
}, [defaultValue, keyRef]); }, [defaultValue, keyRef]);
const wrappedOnChange = useCallback( const wrappedOnChange = useCallback(
(value: string) => { (value: string | null) => {
const payload = data.find((v) => v.value === value)?.payload ?? null; const payload = data.find((v) => v.value === value)?.payload ?? null;
onChange?.(payload); onChange?.(payload);
}, },
@ -93,7 +118,8 @@ export function Selector<T>({
return ( return (
<Select <Select
withinPortal={true} data-testid="input-selector"
comboboxProps={{ withinPortal: true }}
data={data} data={data}
defaultValue={wrappedDefaultValue} defaultValue={wrappedDefaultValue}
value={wrappedValue} value={wrappedValue}
@ -144,6 +170,7 @@ export function MultiSelector<T>({
() => value && value.map(labelRef.current), () => value && value.map(labelRef.current),
[value], [value],
); );
const wrappedDefaultValue = useMemo( const wrappedDefaultValue = useMemo(
() => defaultValue && defaultValue.map(labelRef.current), () => defaultValue && defaultValue.map(labelRef.current),
[defaultValue], [defaultValue],
@ -168,6 +195,7 @@ export function MultiSelector<T>({
return ( return (
<MultiSelect <MultiSelect
{...select} {...select}
hidePickedOptions
value={wrappedValue} value={wrappedValue}
defaultValue={wrappedDefaultValue} defaultValue={wrappedDefaultValue}
onChange={wrappedOnChange} onChange={wrappedOnChange}

@ -1,6 +1,5 @@
import { withModal } from "@/modules/modals"; import { withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { GetItemId } from "@/utilities"; import { GetItemId } from "@/utilities";
import { import {
faCaretDown, faCaretDown,
@ -31,9 +30,7 @@ type SupportType = Item.Movie | Item.Episode;
interface Props<T extends SupportType> { interface Props<T extends SupportType> {
download: (item: T, result: SearchResultType) => Promise<void>; download: (item: T, result: SearchResultType) => Promise<void>;
query: ( query: (id?: number) => UseQueryResult<SearchResultType[] | undefined>;
id?: number,
) => UseQueryResult<SearchResultType[] | undefined, unknown>;
item: T; item: T;
} }
@ -50,7 +47,8 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
const search = useCallback(() => { const search = useCallback(() => {
setSearchStarted(true); setSearchStarted(true);
results.refetch();
void results.refetch();
}, [results]); }, [results]);
const columns = useMemo<Column<SearchResultType>[]>( const columns = useMemo<Column<SearchResultType>[]>(
@ -59,8 +57,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Score", Header: "Score",
accessor: "score", accessor: "score",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value}%</Text>;
return <Text className={classes.noWrap}>{value}%</Text>;
}, },
}, },
{ {
@ -84,13 +81,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Provider", Header: "Provider",
accessor: "provider", accessor: "provider",
Cell: (row) => { Cell: (row) => {
const { classes } = useTableStyles();
const value = row.value; const value = row.value;
const { url } = row.row.original; const { url } = row.row.original;
if (url) { if (url) {
return ( return (
<Anchor <Anchor
className={classes.noWrap} className="table-no-wrap"
href={url} href={url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -107,7 +103,6 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Release", Header: "Release",
accessor: "release_info", accessor: "release_info",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const items = useMemo( const items = useMemo(
@ -116,12 +111,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
); );
if (value.length === 0) { if (value.length === 0) {
return <Text color="dimmed">Cannot get release info</Text>; return <Text c="dimmed">Cannot get release info</Text>;
} }
return ( return (
<Stack spacing={0} onClick={() => setOpen((o) => !o)}> <Stack gap={0} onClick={() => setOpen((o) => !o)}>
<Text className={classes.primary}> <Text className="table-primary" span>
{value[0]} {value[0]}
{value.length > 1 && ( {value.length > 1 && (
<FontAwesomeIcon <FontAwesomeIcon
@ -141,8 +136,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Uploader", Header: "Uploader",
accessor: "uploader", accessor: "uploader",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value ?? "-"}</Text>;
return <Text className={classes.noWrap}>{value ?? "-"}</Text>;
}, },
}, },
{ {

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

@ -1,8 +1,9 @@
import { useIsLoading } from "@/contexts"; import { useIsLoading } from "@/contexts";
import { usePageSize } from "@/utilities/storage"; import { usePageSize } from "@/utilities/storage";
import { Box, createStyles, Skeleton, Table, Text } from "@mantine/core"; import { Box, Skeleton, Table, Text } from "@mantine/core";
import { ReactNode, useMemo } from "react"; import { ReactNode, useMemo } from "react";
import { HeaderGroup, Row, TableInstance } from "react-table"; import { HeaderGroup, Row, TableInstance } from "react-table";
import styles from "./BaseTable.module.scss";
export type BaseTableProps<T extends object> = TableInstance<T> & { export type BaseTableProps<T extends object> = TableInstance<T> & {
tableStyles?: TableStyleProps<T>; tableStyles?: TableStyleProps<T>;
@ -18,37 +19,23 @@ export interface TableStyleProps<T extends object> {
rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>; rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>;
} }
const useStyles = createStyles((theme) => {
return {
container: {
display: "block",
maxWidth: "100%",
overflowX: "auto",
},
table: {
borderCollapse: "collapse",
},
header: {},
};
});
function DefaultHeaderRenderer<T extends object>( function DefaultHeaderRenderer<T extends object>(
headers: HeaderGroup<T>[], headers: HeaderGroup<T>[],
): JSX.Element[] { ): JSX.Element[] {
return headers.map((col) => ( return headers.map((col) => (
<th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}> <Table.Th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
{col.render("Header")} {col.render("Header")}
</th> </Table.Th>
)); ));
} }
function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null { function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null {
return ( return (
<tr {...row.getRowProps()}> <Table.Tr {...row.getRowProps()}>
{row.cells.map((cell) => ( {row.cells.map((cell) => (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td> <Table.Td {...cell.getCellProps()}>{cell.render("Cell")}</Table.Td>
))} ))}
</tr> </Table.Tr>
); );
} }
@ -66,8 +53,6 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer; const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer; const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
const { classes } = useStyles();
const colCount = useMemo(() => { const colCount = useMemo(() => {
return headerGroups.reduce( return headerGroups.reduce(
(prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev), (prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev),
@ -88,19 +73,19 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
body = Array(tableStyles?.placeholder ?? pageSize) body = Array(tableStyles?.placeholder ?? pageSize)
.fill(0) .fill(0)
.map((_, i) => ( .map((_, i) => (
<tr key={i}> <Table.Tr key={i}>
<td colSpan={colCount}> <Table.Td colSpan={colCount}>
<Skeleton height={24}></Skeleton> <Skeleton height={24}></Skeleton>
</td> </Table.Td>
</tr> </Table.Tr>
)); ));
} else if (empty && tableStyles?.emptyText) { } else if (empty && tableStyles?.emptyText) {
body = ( body = (
<tr> <Table.Tr>
<td colSpan={colCount}> <Table.Td colSpan={colCount}>
<Text align="center">{tableStyles.emptyText}</Text> <Text ta="center">{tableStyles.emptyText}</Text>
</td> </Table.Td>
</tr> </Table.Tr>
); );
} else { } else {
body = rows.map((row) => { body = rows.map((row) => {
@ -110,20 +95,20 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
} }
return ( return (
<Box className={classes.container}> <Box className={styles.container}>
<Table <Table
className={classes.table} className={styles.table}
striped={tableStyles?.striped ?? true} striped={tableStyles?.striped ?? true}
{...getTableProps()} {...getTableProps()}
> >
<thead className={classes.header} hidden={tableStyles?.hideHeader}> <Table.Thead hidden={tableStyles?.hideHeader}>
{headerGroups.map((headerGroup) => ( {headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}> <Table.Tr {...headerGroup.getHeaderGroupProps()}>
{headersRenderer(headerGroup.headers)} {headersRenderer(headerGroup.headers)}
</tr> </Table.Tr>
))} ))}
</thead> </Table.Thead>
<tbody {...getTableBodyProps()}>{body}</tbody> <Table.Tbody {...getTableBodyProps()}>{body}</Table.Tbody>
</Table> </Table>
</Box> </Box>
); );

@ -1,6 +1,6 @@
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons"; import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Box, Text } from "@mantine/core"; import { Box, Text, Table } from "@mantine/core";
import { import {
Cell, Cell,
HeaderGroup, HeaderGroup,
@ -29,8 +29,8 @@ function renderRow<T extends object>(row: Row<T>) {
if (cell) { if (cell) {
const rotation = row.isExpanded ? 90 : undefined; const rotation = row.isExpanded ? 90 : undefined;
return ( return (
<tr {...row.getRowProps()}> <Table.Tr {...row.getRowProps()}>
<td {...cell.getCellProps()} colSpan={row.cells.length}> <Table.Td {...cell.getCellProps()} colSpan={row.cells.length}>
<Text {...row.getToggleRowExpandedProps()} p={2}> <Text {...row.getToggleRowExpandedProps()} p={2}>
{cell.render("Cell")} {cell.render("Cell")}
<Box component="span" mx={12}> <Box component="span" mx={12}>
@ -40,21 +40,23 @@ function renderRow<T extends object>(row: Row<T>) {
></FontAwesomeIcon> ></FontAwesomeIcon>
</Box> </Box>
</Text> </Text>
</td> </Table.Td>
</tr> </Table.Tr>
); );
} else { } else {
return null; return null;
} }
} else { } else {
return ( return (
<tr {...row.getRowProps()}> <Table.Tr {...row.getRowProps()}>
{row.cells {row.cells
.filter((cell) => !cell.isPlaceholder) .filter((cell) => !cell.isPlaceholder)
.map((cell) => ( .map((cell) => (
<td {...cell.getCellProps()}>{renderCell(cell, row)}</td> <Table.Td {...cell.getCellProps()}>
{renderCell(cell, row)}
</Table.Td>
))} ))}
</tr> </Table.Tr>
); );
} }
} }
@ -64,7 +66,9 @@ function renderHeaders<T extends object>(
): JSX.Element[] { ): JSX.Element[] {
return headers return headers
.filter((col) => !col.isGrouped) .filter((col) => !col.isGrouped)
.map((col) => <th {...col.getHeaderProps()}>{col.render("Header")}</th>); .map((col) => (
<Table.Th {...col.getHeaderProps()}>{col.render("Header")}</Table.Th>
));
} }
type Props<T extends object> = Omit< type Props<T extends object> = Omit<

@ -28,7 +28,7 @@ const PageControl: FunctionComponent<Props> = ({
}, [total, goto]); }, [total, goto]);
return ( return (
<Group p={16} position="apart"> <Group p={16} justify="apart">
<Text size="sm"> <Text size="sm">
Show {start} to {end} of {total} entries Show {start} to {end} of {total} entries
</Text> </Text>

@ -24,7 +24,7 @@ const ToolboxButton: FunctionComponent<ToolboxButtonProps> = ({
<Button <Button
color="dark" color="dark"
variant="subtle" variant="subtle"
leftIcon={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>} leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
{...props} {...props}
> >
<Text size="xs">{children}</Text> <Text size="xs">{children}</Text>

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

@ -1,15 +1,7 @@
import { createStyles, Group } from "@mantine/core"; import { Group } from "@mantine/core";
import { FunctionComponent, PropsWithChildren } from "react"; import { FunctionComponent, PropsWithChildren } from "react";
import ToolboxButton, { ToolboxMutateButton } from "./Button"; import ToolboxButton, { ToolboxMutateButton } from "./Button";
import styles from "./Toolbox.module.scss";
const useStyles = createStyles((theme) => ({
group: {
backgroundColor:
theme.colorScheme === "light"
? theme.colors.gray[3]
: theme.colors.dark[5],
},
}));
declare type ToolboxComp = FunctionComponent<PropsWithChildren> & { declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
Button: typeof ToolboxButton; Button: typeof ToolboxButton;
@ -17,9 +9,8 @@ declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
}; };
const Toolbox: ToolboxComp = ({ children }) => { const Toolbox: ToolboxComp = ({ children }) => {
const { classes } = useStyles();
return ( return (
<Group p={12} position="apart" className={classes.group}> <Group p={12} justify="apart" className={styles.group}>
{children} {children}
</Group> </Group>
); );

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

@ -27,7 +27,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (msg) => { update: (msg) => {
msg msg
.map((message) => notification.info("Notification", message)) .map((message) => notification.info("Notification", message))
.forEach(showNotification); .forEach((data) => showNotification(data));
}, },
}, },
{ {

@ -133,7 +133,7 @@ class TaskDispatcher {
public removeProgress(ids: string[]) { public removeProgress(ids: string[]) {
setTimeout( setTimeout(
() => ids.forEach(hideNotification), () => ids.forEach((id) => hideNotification(id)),
notification.PROGRESS_TIMEOUT, notification.PROGRESS_TIMEOUT,
); );
} }

@ -1,7 +1,7 @@
import { NotificationProps } from "@mantine/notifications"; import { NotificationData } from "@mantine/notifications";
export const notification = { export const notification = {
info: (title: string, message: string): NotificationProps => { info: (title: string, message: string): NotificationData => {
return { return {
title, title,
message, message,
@ -9,7 +9,7 @@ export const notification = {
}; };
}, },
warn: (title: string, message: string): NotificationProps => { warn: (title: string, message: string): NotificationData => {
return { return {
title, title,
message, message,
@ -18,7 +18,7 @@ export const notification = {
}; };
}, },
error: (title: string, message: string): NotificationProps => { error: (title: string, message: string): NotificationData => {
return { return {
title, title,
message, message,
@ -33,7 +33,7 @@ export const notification = {
pending: ( pending: (
id: string, id: string,
header: string, header: string,
): NotificationProps & { id: string } => { ): NotificationData & { id: string } => {
return { return {
id, id,
title: header, title: header,
@ -48,7 +48,7 @@ export const notification = {
body: string, body: string,
current: number, current: number,
total: number, total: number,
): NotificationProps & { id: string } => { ): NotificationData & { id: string } => {
return { return {
id, id,
title: header, title: header,
@ -57,7 +57,7 @@ export const notification = {
autoClose: false, autoClose: false,
}; };
}, },
end: (id: string, header: string): NotificationProps & { id: string } => { end: (id: string, header: string): NotificationData & { id: string } => {
return { return {
id, id,
title: header, title: header,

@ -52,7 +52,7 @@ const Authentication: FunctionComponent = () => {
{...form.getInputProps("password")} {...form.getInputProps("password")}
></PasswordInput> ></PasswordInput>
<Divider></Divider> <Divider></Divider>
<Button fullWidth uppercase type="submit"> <Button fullWidth tt="uppercase" type="submit">
Login Login
</Button> </Button>
</Stack> </Stack>

@ -3,7 +3,6 @@ import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import { useTableStyles } from "@/styles";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
@ -22,9 +21,8 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
accessor: "title", accessor: "title",
Cell: (row) => { Cell: (row) => {
const target = `/movies/${row.row.original.radarrId}`; const target = `/movies/${row.row.original.radarrId}`;
const { classes } = useTableStyles();
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {row.value}
</Anchor> </Anchor>
); );

@ -3,7 +3,6 @@ import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import { useTableStyles } from "@/styles";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
@ -21,10 +20,9 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
Header: "Series", Header: "Series",
accessor: "seriesTitle", accessor: "seriesTitle",
Cell: (row) => { Cell: (row) => {
const { classes } = useTableStyles();
const target = `/series/${row.row.original.sonarrSeriesId}`; const target = `/series/${row.row.original.sonarrSeriesId}`;
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {row.value}
</Anchor> </Anchor>
); );

@ -125,7 +125,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
<DropContent></DropContent> <DropContent></DropContent>
</Dropzone.FullScreen> </Dropzone.FullScreen>
<Toolbox> <Toolbox>
<Group spacing="xs"> <Group gap="xs">
<Toolbox.Button <Toolbox.Button
icon={faSync} icon={faSync}
disabled={!available || hasTask} disabled={!available || hasTask}
@ -160,7 +160,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
Search Search
</Toolbox.Button> </Toolbox.Button>
</Group> </Group>
<Group spacing="xs"> <Group gap="xs">
<Toolbox.Button <Toolbox.Button
disabled={ disabled={
series === undefined || series === undefined ||

@ -6,7 +6,6 @@ import { AudioList } from "@/components/bazarr";
import { EpisodeHistoryModal } from "@/components/modals"; import { EpisodeHistoryModal } from "@/components/modals";
import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal"; import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { useTableStyles } from "@/styles";
import { BuildKey, filterSubtitleBy } from "@/utilities"; import { BuildKey, filterSubtitleBy } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages"; import { useProfileItemsToLanguages } from "@/utilities/languages";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
@ -92,7 +91,7 @@ const Table: FunctionComponent<Props> = ({
{ {
accessor: "season", accessor: "season",
Cell: (row) => { Cell: (row) => {
return <Text>Season {row.value}</Text>; return <Text span>Season {row.value}</Text>;
}, },
}, },
{ {
@ -103,11 +102,9 @@ const Table: FunctionComponent<Props> = ({
Header: "Title", Header: "Title",
accessor: "title", accessor: "title",
Cell: ({ value, row }) => { Cell: ({ value, row }) => {
const { classes } = useTableStyles();
return ( return (
<TextPopover text={row.original.sceneName}> <TextPopover text={row.original.sceneName}>
<Text className={classes.primary}>{value}</Text> <Text className="table-primary">{value}</Text>
</TextPopover> </TextPopover>
); );
}, },
@ -156,7 +153,7 @@ const Table: FunctionComponent<Props> = ({
}, [episode, seriesId]); }, [episode, seriesId]);
return ( return (
<Group spacing="xs" noWrap> <Group gap="xs" wrap="nowrap">
{elements} {elements}
</Group> </Group>
); );
@ -168,7 +165,7 @@ const Table: FunctionComponent<Props> = ({
Cell: ({ row }) => { Cell: ({ row }) => {
const modals = useModals(); const modals = useModals();
return ( return (
<Group spacing="xs" noWrap> <Group gap="xs" wrap="nowrap">
<Action <Action
label="Manual Search" label="Manual Search"
disabled={disabled} disabled={disabled}

@ -6,7 +6,6 @@ import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon"; import StateIcon from "@/components/StateIcon";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView"; import HistoryView from "@/pages/views/HistoryView";
import { useTableStyles } from "@/styles";
import { import {
faFileExcel, faFileExcel,
faInfoCircle, faInfoCircle,
@ -29,10 +28,9 @@ const MoviesHistoryView: FunctionComponent = () => {
Header: "Name", Header: "Name",
accessor: "title", accessor: "title",
Cell: ({ row, value }) => { Cell: ({ row, value }) => {
const { classes } = useTableStyles();
const target = `/movies/${row.original.radarrId}`; const target = `/movies/${row.original.radarrId}`;
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{value} {value}
</Anchor> </Anchor>
); );

@ -9,7 +9,6 @@ import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon"; import StateIcon from "@/components/StateIcon";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView"; import HistoryView from "@/pages/views/HistoryView";
import { useTableStyles } from "@/styles";
import { import {
faFileExcel, faFileExcel,
faInfoCircle, faInfoCircle,
@ -32,11 +31,10 @@ const SeriesHistoryView: FunctionComponent = () => {
Header: "Series", Header: "Series",
accessor: "seriesTitle", accessor: "seriesTitle",
Cell: (row) => { Cell: (row) => {
const { classes } = useTableStyles();
const target = `/series/${row.row.original.sonarrSeriesId}`; const target = `/series/${row.row.original.sonarrSeriesId}`;
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {row.value}
</Anchor> </Anchor>
); );
@ -50,8 +48,7 @@ const SeriesHistoryView: FunctionComponent = () => {
Header: "Title", Header: "Title",
accessor: "episodeTitle", accessor: "episodeTitle",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value}</Text>;
return <Text className={classes.noWrap}>{value}</Text>;
}, },
}, },
{ {

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

@ -5,16 +5,8 @@ import {
} from "@/apis/hooks"; } from "@/apis/hooks";
import { Selector, Toolbox } from "@/components"; import { Selector, Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async"; import { QueryOverlay } from "@/components/async";
import Language from "@/components/bazarr/Language";
import { Layout } from "@/constants";
import { useSelectorOptions } from "@/utilities"; import { useSelectorOptions } from "@/utilities";
import { import { Box, Container, SimpleGrid, useMantineTheme } from "@mantine/core";
Box,
Container,
SimpleGrid,
createStyles,
useMantineTheme,
} from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { merge } from "lodash"; import { merge } from "lodash";
import { FunctionComponent, useMemo, useState } from "react"; import { FunctionComponent, useMemo, useState } from "react";
@ -29,17 +21,7 @@ import {
YAxis, YAxis,
} from "recharts"; } from "recharts";
import { actionOptions, timeFrameOptions } from "./options"; import { actionOptions, timeFrameOptions } from "./options";
import styles from "./HistoryStats.module.scss";
const useStyles = createStyles((theme) => ({
container: {
display: "flex",
flexDirection: "column",
height: `calc(100vh - ${Layout.HEADER_HEIGHT}px)`,
},
chart: {
height: "90%",
},
}));
const HistoryStats: FunctionComponent = () => { const HistoryStats: FunctionComponent = () => {
const { data: providers } = useSystemProviders(true); const { data: providers } = useSystemProviders(true);
@ -71,8 +53,8 @@ const HistoryStats: FunctionComponent = () => {
date: v.date, date: v.date,
series: v.count, series: v.count,
})); }));
const result = merge(movies, series);
return result; return merge(movies, series);
} else { } else {
return []; return [];
} }
@ -80,20 +62,13 @@ const HistoryStats: FunctionComponent = () => {
useDocumentTitle("History Statistics - Bazarr"); useDocumentTitle("History Statistics - Bazarr");
const { classes } = useStyles();
const theme = useMantineTheme(); const theme = useMantineTheme();
return ( return (
<Container fluid px={0} className={classes.container}> <Container fluid px={0} className={styles.container}>
<QueryOverlay result={stats}> <QueryOverlay result={stats}>
<Toolbox> <Toolbox>
<SimpleGrid <SimpleGrid cols={{ base: 4, xs: 2 }}>
cols={4}
breakpoints={[
{ maxWidth: "sm", cols: 4 },
{ maxWidth: "xs", cols: 2 },
]}
>
<Selector <Selector
placeholder="Time..." placeholder="Time..."
options={timeFrameOptions} options={timeFrameOptions}
@ -123,9 +98,9 @@ const HistoryStats: FunctionComponent = () => {
></Selector> ></Selector>
</SimpleGrid> </SimpleGrid>
</Toolbox> </Toolbox>
<Box className={classes.chart} m="xs"> <Box className={styles.chart} m="xs">
<ResponsiveContainer> <ResponsiveContainer>
<BarChart className={classes.chart} data={convertedData}> <BarChart className={styles.chart} data={convertedData}>
<CartesianGrid strokeDasharray="4 2"></CartesianGrid> <CartesianGrid strokeDasharray="4 2"></CartesianGrid>
<XAxis dataKey="date"></XAxis> <XAxis dataKey="date"></XAxis>
<YAxis allowDecimals={false}></YAxis> <YAxis allowDecimals={false}></YAxis>

@ -1,7 +1,7 @@
import { renderTest, RenderTestCase } from "@/tests/render"; import { renderTest, RenderTestCase } from "@/tests/render";
import MoviesHistoryView from "./Movies"; import MoviesHistoryView from "./Movies";
import SeriesHistoryView from "./Series"; import SeriesHistoryView from "./Series";
import HistoryStats from "./Statistics"; import HistoryStats from "./Statistics/HistoryStats";
const cases: RenderTestCase[] = [ const cases: RenderTestCase[] = [
{ {

@ -123,7 +123,7 @@ const MovieDetailView: FunctionComponent = () => {
<DropContent></DropContent> <DropContent></DropContent>
</Dropzone.FullScreen> </Dropzone.FullScreen>
<Toolbox> <Toolbox>
<Group spacing="xs"> <Group gap="xs">
<Toolbox.Button <Toolbox.Button
icon={faSync} icon={faSync}
disabled={hasTask} disabled={hasTask}
@ -168,7 +168,7 @@ const MovieDetailView: FunctionComponent = () => {
Manual Manual
</Toolbox.Button> </Toolbox.Button>
</Group> </Group>
<Group spacing="xs"> <Group gap="xs">
<Toolbox.Button <Toolbox.Button
disabled={!allowEdit || movie.profileId === null || hasTask} disabled={!allowEdit || movie.profileId === null || hasTask}
icon={faCloudUploadAlt} icon={faCloudUploadAlt}
@ -205,7 +205,7 @@ const MovieDetailView: FunctionComponent = () => {
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item <Menu.Item
icon={<FontAwesomeIcon icon={faToolbox} />} leftSection={<FontAwesomeIcon icon={faToolbox} />}
onClick={() => { onClick={() => {
if (movie) { if (movie) {
modals.openContextModal(SubtitleToolsModal, { modals.openContextModal(SubtitleToolsModal, {
@ -217,7 +217,7 @@ const MovieDetailView: FunctionComponent = () => {
Mass Edit Mass Edit
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<FontAwesomeIcon icon={faHistory} />} leftSection={<FontAwesomeIcon icon={faHistory} />}
onClick={() => { onClick={() => {
if (movie) { if (movie) {
modals.openContextModal(MovieHistoryModal, { movie }); modals.openContextModal(MovieHistoryModal, { movie });

@ -4,7 +4,6 @@ import { Action, SimpleTable } from "@/components";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu"; import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { filterSubtitleBy } from "@/utilities"; import { filterSubtitleBy } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages"; import { useProfileItemsToLanguages } from "@/utilities/languages";
import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons";
@ -40,17 +39,17 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
Header: "Subtitle Path", Header: "Subtitle Path",
accessor: "path", accessor: "path",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles();
const props: TextProps = { const props: TextProps = {
className: classes.primary, className: "table-primary",
}; };
if (isSubtitleTrack(value)) { if (isSubtitleTrack(value)) {
return <Text {...props}>Video File Subtitle Track</Text>; return (
<Text className="table-primary">Video File Subtitle Track</Text>
);
} else if (isSubtitleMissing(value)) { } else if (isSubtitleMissing(value)) {
return ( return (
<Text {...props} color="dimmed"> <Text {...props} c="dimmed">
{value} {value}
</Text> </Text>
); );

@ -6,7 +6,6 @@ import LanguageProfileName from "@/components/bazarr/LanguageProfile";
import { ItemEditModal } from "@/components/forms/ItemEditForm"; import { ItemEditModal } from "@/components/forms/ItemEditForm";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import ItemView from "@/pages/views/ItemView"; import ItemView from "@/pages/views/ItemView";
import { useTableStyles } from "@/styles";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
@ -35,10 +34,9 @@ const MovieView: FunctionComponent = () => {
Header: "Name", Header: "Name",
accessor: "title", accessor: "title",
Cell: ({ row, value }) => { Cell: ({ row, value }) => {
const { classes } = useTableStyles();
const target = `/movies/${row.original.radarrId}`; const target = `/movies/${row.original.radarrId}`;
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{value} {value}
</Anchor> </Anchor>
); );

@ -4,7 +4,6 @@ import LanguageProfileName from "@/components/bazarr/LanguageProfile";
import { ItemEditModal } from "@/components/forms/ItemEditForm"; import { ItemEditModal } from "@/components/forms/ItemEditForm";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import ItemView from "@/pages/views/ItemView"; import ItemView from "@/pages/views/ItemView";
import { useTableStyles } from "@/styles";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -34,10 +33,9 @@ const SeriesView: FunctionComponent = () => {
Header: "Name", Header: "Name",
accessor: "title", accessor: "title",
Cell: ({ row, value }) => { Cell: ({ row, value }) => {
const { classes } = useTableStyles();
const target = `/series/${row.original.sonarrSeriesId}`; const target = `/series/${row.original.sonarrSeriesId}`;
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{value} {value}
</Anchor> </Anchor>
); );
@ -70,13 +68,14 @@ const SeriesView: FunctionComponent = () => {
} }
return ( return (
<Progress <Progress.Root key={title} size="xl">
key={title} <Progress.Section
size="xl" value={progress}
color={episodeMissingCount === 0 ? "brand" : "yellow"} color={episodeMissingCount === 0 ? "brand" : "yellow"}
value={progress} >
label={label} <Progress.Label>{label}</Progress.Label>
></Progress> </Progress.Section>
</Progress.Root>
); );
}, },
}, },

@ -4,7 +4,7 @@ import {
faClipboard, faClipboard,
faSync, faSync,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { Group as MantineGroup, Text as MantineText } from "@mantine/core"; import { Box, Group as MantineGroup, Text as MantineText } from "@mantine/core";
import { useClipboard } from "@mantine/hooks"; import { useClipboard } from "@mantine/hooks";
import { FunctionComponent, useState } from "react"; import { FunctionComponent, useState } from "react";
import { import {
@ -54,7 +54,7 @@ const SettingsGeneralView: FunctionComponent = () => {
></Number> ></Number>
<Text <Text
label="Base URL" label="Base URL"
icon="/" leftSection="/"
settingKey="settings-general-base_url" settingKey="settings-general-base_url"
settingOptions={{ settingOptions={{
onLoaded: (s) => s.general.base_url?.slice(1) ?? "", onLoaded: (s) => s.general.base_url?.slice(1) ?? "",
@ -87,7 +87,7 @@ const SettingsGeneralView: FunctionComponent = () => {
rightSectionWidth={95} rightSectionWidth={95}
rightSectionProps={{ style: { justifyContent: "flex-end" } }} rightSectionProps={{ style: { justifyContent: "flex-end" } }}
rightSection={ rightSection={
<MantineGroup spacing="xs" mx="xs" position="right"> <MantineGroup gap="xs" mx="xs" justify="right">
{ {
// Clipboard API is only available in secure contexts See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#interfaces // Clipboard API is only available in secure contexts See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#interfaces
window.isSecureContext && ( window.isSecureContext && (
@ -204,13 +204,12 @@ const SettingsGeneralView: FunctionComponent = () => {
<Number <Number
label="Retention" label="Retention"
settingKey="settings-backup-retention" settingKey="settings-backup-retention"
styles={{
rightSection: { width: "4rem", justifyContent: "flex-end" },
}}
rightSection={ rightSection={
<MantineText size="xs" px="sm" color="dimmed"> <Box w="4rem" style={{ justifyContent: "flex-end" }}>
Days <MantineText size="xs" px="sm" c="dimmed">
</MantineText> Days
</MantineText>
</Box>
} }
></Number> ></Number>
</Section> </Section>

@ -355,7 +355,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
return ( return (
<> <>
<SimpleTable data={equals} columns={columns}></SimpleTable> <SimpleTable data={equals} columns={columns}></SimpleTable>
<Button fullWidth disabled={!canAdd} color="light" onClick={add}> <Button fullWidth disabled={!canAdd} onClick={add}>
{canAdd ? "Add Equal" : "No Enabled Languages"} {canAdd ? "Add Equal" : "No Enabled Languages"}
</Button> </Button>
</> </>

@ -70,7 +70,7 @@ const Table: FunctionComponent = () => {
const items = row.value; const items = row.value;
const cutoff = row.row.original.cutoff; const cutoff = row.row.original.cutoff;
return ( return (
<Group spacing="xs" noWrap> <Group gap="xs" wrap="nowrap">
{items.map((v) => { {items.map((v) => {
const isCutoff = v.id === cutoff || cutoff === anyCutoff; const isCutoff = v.id === cutoff || cutoff === anyCutoff;
return ( return (
@ -128,7 +128,7 @@ const Table: FunctionComponent = () => {
Cell: ({ row }) => { Cell: ({ row }) => {
const profile = row.original; const profile = row.original;
return ( return (
<Group spacing="xs" noWrap> <Group gap="xs" wrap="nowrap">
<Action <Action
label="Edit Profile" label="Edit Profile"
icon={faWrench} icon={faWrench}
@ -163,7 +163,6 @@ const Table: FunctionComponent = () => {
<Button <Button
fullWidth fullWidth
disabled={!canAdd} disabled={!canAdd}
color="light"
onClick={() => { onClick={() => {
const profile = { const profile = {
profileId: nextProfileId, profileId: nextProfileId,

@ -90,7 +90,7 @@ const NotificationForm: FunctionComponent<Props> = ({
></Textarea> ></Textarea>
</div> </div>
<Divider></Divider> <Divider></Divider>
<Group position="right"> <Group justify="right">
<MutateButton mutation={test} args={() => form.values.url}> <MutateButton mutation={test} args={() => form.values.url}>
Test Test
</MutateButton> </MutateButton>

@ -9,12 +9,12 @@ import {
Text as MantineText, Text as MantineText,
SimpleGrid, SimpleGrid,
Stack, Stack,
AutocompleteProps,
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { capitalize } from "lodash"; import { capitalize } from "lodash";
import { import {
FunctionComponent, FunctionComponent,
forwardRef,
useCallback, useCallback,
useMemo, useMemo,
useRef, useRef,
@ -50,6 +50,11 @@ interface ProviderViewProps {
settingsKey: SettingsKey; settingsKey: SettingsKey;
} }
interface ProviderSelect {
value: string;
payload: ProviderInfo;
}
export const ProviderView: FunctionComponent<ProviderViewProps> = ({ export const ProviderView: FunctionComponent<ProviderViewProps> = ({
availableOptions, availableOptions,
settingsKey, settingsKey,
@ -130,17 +135,16 @@ interface ProviderToolProps {
settingsKey: Readonly<SettingsKey>; settingsKey: Readonly<SettingsKey>;
} }
const SelectItem = forwardRef< const SelectItem: AutocompleteProps["renderOption"] = ({ option }) => {
HTMLDivElement, const provider = option as ProviderSelect;
{ payload: ProviderInfo; label: string }
>(({ payload: { description }, label, ...other }, ref) => {
return ( return (
<Stack spacing={1} ref={ref} {...other}> <Stack gap={1}>
<MantineText size="md">{label}</MantineText> <MantineText size="md">{provider.value}</MantineText>
<MantineText size="xs">{description}</MantineText> <MantineText size="xs">{provider.payload.description}</MantineText>
</Stack> </Stack>
); );
}); };
const ProviderTool: FunctionComponent<ProviderToolProps> = ({ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
payload, payload,
@ -298,19 +302,19 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
} }
}); });
return <Stack spacing="xs">{elements}</Stack>; return <Stack gap="xs">{elements}</Stack>;
}, [info]); }, [info]);
return ( return (
<SettingsProvider value={settings}> <SettingsProvider value={settings}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<Stack> <Stack>
<Stack spacing="xs"> <Stack gap="xs">
<Selector <Selector
data-autofocus data-autofocus
searchable searchable
placeholder="Click to Select a Provider" placeholder="Click to Select a Provider"
itemComponent={SelectItem} renderOption={SelectItem}
disabled={payload !== null} disabled={payload !== null}
{...selectorOptions} {...selectorOptions}
value={info} value={info}
@ -323,7 +327,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
</div> </div>
</Stack> </Stack>
<Divider></Divider> <Divider></Divider>
<Group position="right"> <Group justify="right">
<Button hidden={!payload} color="red" onClick={deletePayload}> <Button hidden={!payload} color="red" onClick={deletePayload}>
Delete Delete
</Button> </Button>

@ -30,7 +30,7 @@ const SettingsRadarrView: FunctionComponent = () => {
<Number label="Port" settingKey="settings-radarr-port"></Number> <Number label="Port" settingKey="settings-radarr-port"></Number>
<Text <Text
label="Base URL" label="Base URL"
icon="/" leftSection="/"
settingKey="settings-radarr-base_url" settingKey="settings-radarr-base_url"
settingOptions={{ settingOptions={{
onLoaded: (s) => s.radarr.base_url?.slice(1) ?? "", onLoaded: (s) => s.radarr.base_url?.slice(1) ?? "",

@ -32,7 +32,7 @@ const SettingsSonarrView: FunctionComponent = () => {
<Number label="Port" settingKey="settings-sonarr-port"></Number> <Number label="Port" settingKey="settings-sonarr-port"></Number>
<Text <Text
label="Base URL" label="Base URL"
icon="/" leftSection="/"
settingKey="settings-sonarr-base_url" settingKey="settings-sonarr-base_url"
settingOptions={{ settingOptions={{
onLoaded: (s) => s.sonarr.base_url?.slice(1) ?? "", onLoaded: (s) => s.sonarr.base_url?.slice(1) ?? "",

@ -501,7 +501,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
label="Command" label="Command"
settingKey="settings-general-postprocessing_cmd" settingKey="settings-general-postprocessing_cmd"
></Text> ></Text>
<Table highlightOnHover fontSize="sm"> <Table highlightOnHover fs="sm">
<tbody>{commandOptionElements}</tbody> <tbody>{commandOptionElements}</tbody>
</Table> </Table>
</CollapseBox> </CollapseBox>

@ -0,0 +1,9 @@
.card {
border-radius: var(--mantine-radius-sm);
border: 1px solid var(--mantine-color-gray-7);
&:hover {
box-shadow: var(--mantine-shadow-md);
border: 1px solid $color-brand-5;
}
}

@ -1,30 +1,8 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
Center,
createStyles,
Stack,
Text,
UnstyledButton,
} from "@mantine/core";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import styles from "./Card.module.scss";
const useCardStyles = createStyles((theme) => {
return {
card: {
borderRadius: theme.radius.sm,
border: `1px solid ${theme.colors.gray[7]}`,
"&:hover": {
boxShadow: theme.shadows.md,
border: `1px solid ${theme.colors.brand[5]}`,
},
},
stack: {
height: "100%",
},
};
});
interface CardProps { interface CardProps {
header?: string; header?: string;
@ -39,16 +17,15 @@ export const Card: FunctionComponent<CardProps> = ({
plus, plus,
onClick, onClick,
}) => { }) => {
const { classes } = useCardStyles();
return ( return (
<UnstyledButton p="lg" onClick={onClick} className={classes.card}> <UnstyledButton p="lg" onClick={onClick} className={styles.card}>
{plus ? ( {plus ? (
<Center> <Center>
<FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon> <FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon>
</Center> </Center>
) : ( ) : (
<Stack className={classes.stack} spacing={0} align="flex-start"> <Stack h="100%" gap={0} align="flex-start">
<Text weight="bold">{header}</Text> <Text fw="bold">{header}</Text>
<Text hidden={description === undefined}>{description}</Text> <Text hidden={description === undefined}>{description}</Text>
</Stack> </Stack>
)} )}

@ -73,7 +73,7 @@ const Layout: FunctionComponent<Props> = (props) => {
icon={faSave} icon={faSave}
loading={isMutating} loading={isMutating}
disabled={totalStagedCount === 0} disabled={totalStagedCount === 0}
rightIcon={ rightSection={
<Badge size="xs" radius="sm" hidden={totalStagedCount === 0}> <Badge size="xs" radius="sm" hidden={totalStagedCount === 0}>
{totalStagedCount} {totalStagedCount}
</Badge> </Badge>

@ -74,7 +74,7 @@ const LayoutModal: FunctionComponent<Props> = (props) => {
<Space h="md" /> <Space h="md" />
<Divider></Divider> <Divider></Divider>
<Space h="md" /> <Space h="md" />
<Group position="right"> <Group justify="right">
<Button <Button
type="submit" type="submit"
disabled={totalStagedCount === 0} disabled={totalStagedCount === 0}

@ -12,7 +12,7 @@ export const Message: FunctionComponent<Props> = ({
children, children,
}) => { }) => {
return ( return (
<Text size="sm" color={type === "info" ? "dimmed" : "yellow"} my={0}> <Text size="sm" c={type === "info" ? "dimmed" : "yellow"} my={0}>
{children} {children}
</Text> </Text>
); );

@ -1,4 +1,4 @@
import { rawRender, screen } from "@/tests"; import { render, screen } from "@/tests";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { describe, it } from "vitest"; import { describe, it } from "vitest";
import { Section } from "./Section"; import { Section } from "./Section";
@ -6,7 +6,7 @@ import { Section } from "./Section";
describe("Settings section", () => { describe("Settings section", () => {
const header = "Section Header"; const header = "Section Header";
it("should show header", () => { it("should show header", () => {
rawRender(<Section header="Section Header"></Section>); render(<Section header="Section Header"></Section>);
expect(screen.getByText(header)).toBeDefined(); expect(screen.getByText(header)).toBeDefined();
expect(screen.getByRole("separator")).toBeDefined(); expect(screen.getByRole("separator")).toBeDefined();
@ -14,7 +14,7 @@ describe("Settings section", () => {
it("should show children", () => { it("should show children", () => {
const text = "Section Child"; const text = "Section Child";
rawRender( render(
<Section header="Section Header"> <Section header="Section Header">
<Text>{text}</Text> <Text>{text}</Text>
</Section>, </Section>,
@ -26,7 +26,7 @@ describe("Settings section", () => {
it("should work with hidden", () => { it("should work with hidden", () => {
const text = "Section Child"; const text = "Section Child";
rawRender( render(
<Section header="Section Header" hidden> <Section header="Section Header" hidden>
<Text>{text}</Text> <Text>{text}</Text>
</Section>, </Section>,

@ -14,7 +14,7 @@ export const Section: FunctionComponent<Props> = ({
children, children,
}) => { }) => {
return ( return (
<Stack hidden={hidden} spacing="xs" my="lg"> <Stack hidden={hidden} gap="xs" my="lg">
<Title order={4}>{header}</Title> <Title order={4}>{header}</Title>
<Divider></Divider> <Divider></Divider>
{children} {children}

@ -31,7 +31,7 @@ const CollapseBox: FunctionComponent<Props> = ({
return ( return (
<Collapse in={open} pl={indent ? "md" : undefined}> <Collapse in={open} pl={indent ? "md" : undefined}>
<Stack spacing="xs">{children}</Stack> <Stack gap="xs">{children}</Stack>
</Collapse> </Collapse>
); );
}; };

@ -1,4 +1,4 @@
import { rawRender, RenderOptions, screen } from "@/tests"; import { render, RenderOptions, screen } from "@/tests";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { FunctionComponent, PropsWithChildren, ReactElement } from "react"; import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
import { describe, it } from "vitest"; import { describe, it } from "vitest";
@ -18,7 +18,7 @@ const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
const formRender = ( const formRender = (
ui: ReactElement, ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">, options?: Omit<RenderOptions, "wrapper">,
) => rawRender(ui, { wrapper: FormSupport, ...options }); ) => render(<FormSupport>{ui}</FormSupport>);
describe("Settings form", () => { describe("Settings form", () => {
describe("number component", () => { describe("number component", () => {

@ -38,6 +38,11 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
if (val === "") { if (val === "") {
val = 0; val = 0;
} }
if (typeof val === "string") {
return update(+val);
}
update(val); update(val);
}} }}
></NumberInput> ></NumberInput>

@ -56,7 +56,7 @@ export const URLTestButton: FunctionComponent<{
}, [address, port, url, apikey, ssl]); }, [address, port, url, apikey, ssl]);
return ( return (
<Button onClick={click} color={color} title={title}> <Button autoContrast onClick={click} variant={color} title={title}>
{title} {title}
</Button> </Button>
); );
@ -107,7 +107,7 @@ export const ProviderTestButton: FunctionComponent<{
}, [testUrl]); }, [testUrl]);
return ( return (
<Button onClick={click} color={color} title={title}> <Button onClick={click} variant={color} title={title}>
{title} {title}
</Button> </Button>
); );

@ -141,7 +141,7 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
columns={columns} columns={columns}
data={data} data={data}
></SimpleTable> ></SimpleTable>
<Button fullWidth color="light" onClick={addRow}> <Button fullWidth onClick={addRow}>
Add Add
</Button> </Button>
</> </>

@ -1,7 +1,6 @@
import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks"; import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks";
import { SimpleTable } from "@/components"; import { SimpleTable } from "@/components";
import { MutateAction } from "@/components/async"; import { MutateAction } from "@/components/async";
import { useTableStyles } from "@/styles";
import { faWindowClose } from "@fortawesome/free-solid-svg-icons"; import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
@ -20,16 +19,14 @@ const Table: FunctionComponent<Props> = ({ announcements }) => {
Header: "Since", Header: "Since",
accessor: "timestamp", accessor: "timestamp",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-primary">{value}</Text>;
return <Text className={classes.primary}>{value}</Text>;
}, },
}, },
{ {
Header: "Announcement", Header: "Announcement",
accessor: "text", accessor: "text",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-primary">{value}</Text>;
return <Text className={classes.primary}>{value}</Text>;
}, },
}, },
{ {

@ -1,7 +1,6 @@
import { useDeleteBackups, useRestoreBackups } from "@/apis/hooks"; import { useDeleteBackups, useRestoreBackups } from "@/apis/hooks";
import { Action, PageTable } from "@/components"; import { Action, PageTable } from "@/components";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { useTableStyles } from "@/styles";
import { Environment } from "@/utilities"; import { Environment } from "@/utilities";
import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
@ -32,16 +31,14 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
Header: "Size", Header: "Size",
accessor: "size", accessor: "size",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value}</Text>;
return <Text className={classes.noWrap}>{value}</Text>;
}, },
}, },
{ {
Header: "Time", Header: "Time",
accessor: "date", accessor: "date",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value}</Text>;
return <Text className={classes.noWrap}>{value}</Text>;
}, },
}, },
{ {

@ -86,7 +86,7 @@ const SystemLogsView: FunctionComponent = () => {
<Container fluid px={0}> <Container fluid px={0}>
<QueryOverlay result={logs}> <QueryOverlay result={logs}>
<Toolbox> <Toolbox>
<Group spacing="xs"> <Group gap="xs">
<Toolbox.Button <Toolbox.Button
loading={isFetching} loading={isFetching}
icon={faSync} icon={faSync}
@ -108,7 +108,7 @@ const SystemLogsView: FunctionComponent = () => {
loading={isLoading} loading={isLoading}
icon={faFilter} icon={faFilter}
onClick={openFilterModal} onClick={openFilterModal}
rightIcon={ rightSection={
suffix() !== "" ? ( suffix() !== "" ? (
<Badge size="xs" radius="sm"> <Badge size="xs" radius="sm">
{suffix()} {suffix()}

@ -23,7 +23,7 @@ const SystemReleasesView: FunctionComponent = () => {
return ( return (
<Container size={600} py={12}> <Container size={600} py={12}>
<QueryOverlay result={releases}> <QueryOverlay result={releases}>
<Stack spacing="lg"> <Stack gap="lg">
{data?.map((v, idx) => ( {data?.map((v, idx) => (
<ReleaseCard key={BuildKey(idx, v.date)} {...v}></ReleaseCard> <ReleaseCard key={BuildKey(idx, v.date)} {...v}></ReleaseCard>
))} ))}
@ -47,7 +47,7 @@ const ReleaseCard: FunctionComponent<ReleaseInfo> = ({
return ( return (
<Card shadow="md" p="lg"> <Card shadow="md" p="lg">
<Group> <Group>
<Text weight="bold">{name}</Text> <Text fw="bold">{name}</Text>
<Badge color="blue">{date}</Badge> <Badge color="blue">{date}</Badge>
<Badge color={prerelease ? "yellow" : "green"}> <Badge color={prerelease ? "yellow" : "green"}>
{prerelease ? "Development" : "Master"} {prerelease ? "Development" : "Master"}

@ -46,7 +46,7 @@ function Row(props: InfoProps): JSX.Element {
return ( return (
<Grid columns={10}> <Grid columns={10}>
<Grid.Col span={2}> <Grid.Col span={2}>
<Text size="sm" align="right" weight="bold"> <Text size="sm" ta="right" fw="bold">
{title} {title}
</Text> </Text>
</Grid.Col> </Grid.Col>
@ -85,9 +85,12 @@ const InfoContainer: FunctionComponent<
return ( return (
<Stack> <Stack>
<Divider <Divider
labelProps={{ size: "medium", weight: "bold" }}
labelPosition="left" labelPosition="left"
label={title} label={
<Text size="md" fw="bold">
{title}
</Text>
}
></Divider> ></Divider>
{children} {children}
<Space /> <Space />

@ -1,5 +1,4 @@
import { SimpleTable } from "@/components"; import { SimpleTable } from "@/components";
import { useTableStyles } from "@/styles";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table"; import { Column } from "react-table";
@ -15,16 +14,14 @@ const Table: FunctionComponent<Props> = ({ health }) => {
Header: "Object", Header: "Object",
accessor: "object", accessor: "object",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value}</Text>;
return <Text className={classes.noWrap}>{value}</Text>;
}, },
}, },
{ {
Header: "Issue", Header: "Issue",
accessor: "issue", accessor: "issue",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-primary">{value}</Text>;
return <Text className={classes.primary}>{value}</Text>;
}, },
}, },
], ],

@ -1,7 +1,6 @@
import { useRunTask } from "@/apis/hooks"; import { useRunTask } from "@/apis/hooks";
import { SimpleTable } from "@/components"; import { SimpleTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import { useTableStyles } from "@/styles";
import { faPlay } from "@fortawesome/free-solid-svg-icons"; import { faPlay } from "@fortawesome/free-solid-svg-icons";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
@ -18,16 +17,14 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
Header: "Name", Header: "Name",
accessor: "name", accessor: "name",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-primary">{value}</Text>;
return <Text className={classes.primary}>{value}</Text>;
}, },
}, },
{ {
Header: "Interval", Header: "Interval",
accessor: "interval", accessor: "interval",
Cell: ({ value }) => { Cell: ({ value }) => {
const { classes } = useTableStyles(); return <Text className="table-no-wrap">{value}</Text>;
return <Text className={classes.noWrap}>{value}</Text>;
}, },
}, },
{ {

@ -39,7 +39,7 @@ const WantedMoviesView: FunctionComponent = () => {
const { download } = useMovieSubtitleModification(); const { download } = useMovieSubtitleModification();
return ( return (
<Group spacing="sm"> <Group gap="sm">
{value.map((item, idx) => ( {value.map((item, idx) => (
<Badge <Badge
color={download.isLoading ? "gray" : undefined} color={download.isLoading ? "gray" : undefined}

@ -6,7 +6,6 @@ import {
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import { TaskGroup, task } from "@/modules/task"; import { TaskGroup, task } from "@/modules/task";
import WantedView from "@/pages/views/WantedView"; import WantedView from "@/pages/views/WantedView";
import { useTableStyles } from "@/styles";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -23,9 +22,8 @@ const WantedSeriesView: FunctionComponent = () => {
accessor: "seriesTitle", accessor: "seriesTitle",
Cell: (row) => { Cell: (row) => {
const target = `/series/${row.row.original.sonarrSeriesId}`; const target = `/series/${row.row.original.sonarrSeriesId}`;
const { classes } = useTableStyles();
return ( return (
<Anchor className={classes.primary} component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {row.value}
</Anchor> </Anchor>
); );
@ -49,7 +47,7 @@ const WantedSeriesView: FunctionComponent = () => {
const { download } = useEpisodeSubtitleModification(); const { download } = useEpisodeSubtitleModification();
return ( return (
<Group spacing="sm"> <Group gap="sm">
{value.map((item, idx) => ( {value.map((item, idx) => (
<Badge <Badge
color={download.isLoading ? "gray" : undefined} color={download.isLoading ? "gray" : undefined}

@ -45,13 +45,11 @@ const UIError: FunctionComponent<Props> = ({ error }) => {
<Center my="xl"> <Center my="xl">
<Code>{stack}</Code> <Code>{stack}</Code>
</Center> </Center>
<Group position="center"> <Group justify="center">
<Anchor href={`${GithubRepoRoot}/issues/new/choose`} target="_blank"> <Anchor href={`${GithubRepoRoot}/issues/new/choose`} target="_blank">
<Button color="yellow">Report Issue</Button> <Button color="yellow">Report Issue</Button>
</Anchor> </Anchor>
<Button onClick={Reload} color="light"> <Button onClick={Reload}>Reload Page</Button>
Reload Page
</Button>
</Group> </Group>
</Container> </Container>
); );

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save