Merge branch 'refs/heads/development' into non-hi-only

pull/2475/head
morpheus65535 6 months ago
commit d24ccbacd1

@ -165,6 +165,9 @@ def apply_update():
parent_dir = os.path.dirname(file_path)
os.makedirs(parent_dir, exist_ok=True)
if not os.path.isdir(file_path):
if os.path.exists(file_path):
# remove the file first to handle case-insensitive file systems
os.remove(file_path)
with open(file_path, 'wb+') as f:
f.write(archive.read(file))
except Exception:
@ -229,6 +232,9 @@ def update_cleaner(zipfile, bazarr_dir, config_dir):
dir_to_ignore_regex = re.compile(dir_to_ignore_regex_string)
file_to_ignore = ['nssm.exe', '7za.exe', 'unins000.exe', 'unins000.dat']
# prevent deletion of leftover Apprise.py/pyi files after 1.8.0 version that caused issue on case-insensitive
# filesystem. This could be removed in a couple of major versions.
file_to_ignore += ['Apprise.py', 'Apprise.pyi', 'apprise.py', 'apprise.pyi']
logging.debug(f'BAZARR upgrade leftover cleaner will ignore those files: {", ".join(file_to_ignore)}')
extension_to_ignore = ['.pyc']
logging.debug(

@ -496,7 +496,7 @@ def get_throttled_providers():
except Exception:
# set empty content in throttled_providers.dat
logging.error("Invalid content in throttled_providers.dat. Resetting")
set_throttled_providers(providers)
set_throttled_providers(str(providers))
finally:
return providers

@ -1,6 +1,6 @@
# coding=utf-8
import apprise
from apprise import Apprise, AppriseAsset
import logging
from .database import TableSettingsNotifier, TableEpisodes, TableShows, TableMovies, database, insert, delete, select
@ -8,7 +8,7 @@ from .database import TableSettingsNotifier, TableEpisodes, TableShows, TableMov
def update_notifier():
# define apprise object
a = apprise.Apprise()
a = Apprise()
# Retrieve all the details
results = a.details()
@ -70,9 +70,9 @@ def send_notifications(sonarr_series_id, sonarr_episode_id, message):
if not episode:
return
asset = apprise.AppriseAsset(async_mode=False)
asset = AppriseAsset(async_mode=False)
apobj = apprise.Apprise(asset=asset)
apobj = Apprise(asset=asset)
for provider in providers:
if provider.url is not None:
@ -101,9 +101,9 @@ def send_notifications_movie(radarr_id, message):
else:
movie_year = ''
asset = apprise.AppriseAsset(async_mode=False)
asset = AppriseAsset(async_mode=False)
apobj = apprise.Apprise(asset=asset)
apobj = Apprise(asset=asset)
for provider in providers:
if provider.url is not None:

@ -3,6 +3,7 @@
# only methods can be specified here that do not cause other moudules to be loaded
# for other methods that use settings, etc., use utilities/helper.py
import contextlib
import logging
import os
from pathlib import Path
@ -53,4 +54,8 @@ def restart_bazarr():
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
logging.info('Bazarr is being restarted...')
raise SystemExit(EXIT_NORMAL)
# Wrap the SystemExit for a graceful restart. The SystemExit still performs the cleanup but the traceback is omitted
# preventing to throw the exception to the caller but still terminates the Python process with the desired Exit Code
with contextlib.suppress(SystemExit):
raise SystemExit(EXIT_NORMAL)

@ -946,8 +946,8 @@ def _search_external_subtitles(path, languages=None, only_one=False, match_stric
lambda m: "" if str(m.group(1)).lower() in FULL_LANGUAGE_LIST else m.group(0), p_root)
p_root_lower = p_root_bare.lower()
filename_matches = p_root_lower == fn_no_ext_lower
# comparing to both unicode normalization forms to prevent broking stuff and improve indexing on some platforms.
filename_matches = fn_no_ext_lower in [p_root_lower, unicodedata.normalize('NFC', p_root_lower)]
filename_contains = p_root_lower in fn_no_ext_lower
if not filename_matches:
@ -1193,7 +1193,7 @@ def save_subtitles(file_path, subtitles, single=False, directory=None, chmod=Non
must_remove_hi = 'remove_HI' in subtitle.mods
# check content
if subtitle.content is None:
if subtitle.content is None or subtitle.text is None:
logger.error('Skipping subtitle %r: no content', subtitle)
continue
@ -1203,7 +1203,7 @@ def save_subtitles(file_path, subtitles, single=False, directory=None, chmod=Non
continue
# create subtitle path
if bool(re.search(HI_REGEX, subtitle.text)):
if subtitle.text and bool(re.search(HI_REGEX, subtitle.text)):
subtitle.language.hi = True
subtitle_path = get_subtitle_path(file_path, None if single else subtitle.language,
forced_tag=subtitle.language.forced,

@ -126,7 +126,7 @@ class SubdivxSubtitlesProvider(Provider):
titles = [video.series if episode else video.title]
try:
titles.extend(video.alternative_titles)
titles.extend(video.alternative_series if episode else video.alternative_titles)
except:
pass
else:
@ -138,6 +138,7 @@ class SubdivxSubtitlesProvider(Provider):
# TODO: cache pack queries (TV SHOW S01).
# Too many redundant server calls.
for title in titles:
title = _series_sanitizer(title)
for query in (
f"{title} S{video.season:02}E{video.episode:02}",
f"{title} S{video.season:02}",
@ -297,20 +298,31 @@ def _check_episode(video, title):
) and season_num == video.season
series_title = _SERIES_RE.sub("", title).strip()
series_title = _series_sanitizer(series_title)
distance = abs(len(series_title) - len(video.series))
for video_series_title in [video.series] + video.alternative_series:
video_series_title = _series_sanitizer(video_series_title)
distance = abs(len(series_title) - len(video_series_title))
series_matched = distance < 4 and ep_matches
series_matched = (distance < 4 or video_series_title in series_title) and ep_matches
logger.debug(
"Series matched? %s [%s -> %s] [title distance: %d]",
series_matched,
video_series_title,
series_title,
distance,
)
if series_matched:
return True
return False
logger.debug(
"Series matched? %s [%s -> %s] [title distance: %d]",
series_matched,
video,
title,
distance,
)
return series_matched
def _series_sanitizer(title):
title = re.sub(r"\'|\.+", '', title) # remove single quote and dot
title = re.sub(r"\W+", ' ', title) # replace by a space anything other than a letter, digit or underscore
return re.sub(r"([A-Z])\s(?=[A-Z]\b)", '', title).strip() # Marvels Agent of S.H.I.E.L.D
def _check_movie(video, title):

File diff suppressed because it is too large Load Diff

@ -13,12 +13,12 @@
},
"private": true,
"dependencies": {
"@mantine/core": "^6.0.21",
"@mantine/dropzone": "^6.0.21",
"@mantine/form": "^6.0.21",
"@mantine/hooks": "^6.0.21",
"@mantine/modals": "^6.0.21",
"@mantine/notifications": "^6.0.21",
"@mantine/core": "^7.10.1",
"@mantine/dropzone": "^7.10.1",
"@mantine/form": "^7.10.1",
"@mantine/hooks": "^7.10.1",
"@mantine/modals": "^7.10.1",
"@mantine/notifications": "^7.10.1",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -37,7 +37,7 @@
"@testing-library/react": "^15.0.5",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.0",
"@types/lodash": "^4.17.1",
"@types/node": "^20.12.6",
"@types/react": "^18.2.75",
"@types/react-dom": "^18.2.24",
@ -53,6 +53,8 @@
"husky": "^9.0.11",
"jsdom": "^24.0.0",
"lodash": "^4.17.21",
"postcss-preset-mantine": "^1.14.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"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 { Action, Search } from "@/components";
import { Layout } from "@/constants";
import { useNavbar } from "@/contexts/Navbar";
import { useIsOnline } from "@/contexts/Online";
import { Environment, useGotoHomepage } from "@/utilities";
@ -12,27 +11,16 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Anchor,
AppShell,
Avatar,
Badge,
Burger,
Divider,
Group,
Header,
MediaQuery,
Menu,
createStyles,
} from "@mantine/core";
import { FunctionComponent } from "react";
const useStyles = createStyles((theme) => {
const headerBackgroundColor =
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[4];
return {
header: {
backgroundColor: headerBackgroundColor,
},
};
});
import styles from "./Header.module.scss";
const AppHeader: FunctionComponent = () => {
const { data: settings } = useSystemSettings();
@ -47,39 +35,28 @@ const AppHeader: FunctionComponent = () => {
const goHome = useGotoHomepage();
const { classes } = useStyles();
return (
<Header p="md" height={Layout.HEADER_HEIGHT} className={classes.header}>
<Group position="apart" noWrap>
<Group noWrap>
<MediaQuery
smallerThan={Layout.MOBILE_BREAKPOINT}
styles={{ display: "none" }}
>
<Anchor onClick={goHome}>
<Avatar
alt="brand"
size={32}
src={`${Environment.baseUrl}/images/logo64.png`}
></Avatar>
</Anchor>
</MediaQuery>
<MediaQuery
largerThan={Layout.MOBILE_BREAKPOINT}
styles={{ display: "none" }}
>
<Burger
opened={showed}
onClick={() => show(!showed)}
size="sm"
></Burger>
</MediaQuery>
<AppShell.Header p="md" className={styles.header}>
<Group justify="space-between" wrap="nowrap">
<Group wrap="nowrap">
<Anchor onClick={goHome} visibleFrom="sm">
<Avatar
alt="brand"
size={32}
src={`${Environment.baseUrl}/images/logo64.png`}
></Avatar>
</Anchor>
<Burger
opened={showed}
onClick={() => show(!showed)}
size="sm"
hiddenFrom="sm"
></Burger>
<Badge size="lg" radius="sm">
Bazarr
</Badge>
</Group>
<Group spacing="xs" position="right" noWrap>
<Group gap="xs" justify="right" wrap="nowrap">
<Search></Search>
<Menu>
<Menu.Target>
@ -95,13 +72,13 @@ const AppHeader: FunctionComponent = () => {
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
leftSection={<FontAwesomeIcon icon={faArrowRotateLeft} />}
onClick={() => restart()}
>
Restart
</Menu.Item>
<Menu.Item
icon={<FontAwesomeIcon icon={faPowerOff} />}
leftSection={<FontAwesomeIcon icon={faPowerOff} />}
onClick={() => shutdown()}
>
Shutdown
@ -114,7 +91,7 @@ const AppHeader: FunctionComponent = () => {
</Menu>
</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 { Layout } from "@/constants";
import { useNavbar } from "@/contexts/Navbar";
import { useRouteItems } from "@/Router";
import { CustomRouteObject, Route } from "@/Router/type";
@ -14,19 +13,19 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Anchor,
AppShell,
Badge,
Collapse,
createStyles,
Divider,
Group,
Navbar as MantineNavbar,
Stack,
Text,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core";
import { useHover } from "@mantine/hooks";
import clsx from "clsx";
import {
import React, {
createContext,
FunctionComponent,
useContext,
@ -35,6 +34,7 @@ import {
useState,
} from "react";
import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom";
import styles from "./Navbar.module.scss";
const Selection = createContext<{
selection: string | null;
@ -97,11 +97,12 @@ function useIsActive(parent: string, route: RouteObject) {
}
const AppNavbar: FunctionComponent = () => {
const { showed } = useNavbar();
const [selection, select] = useState<string | null>(null);
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const { toggleColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light");
const dark = computedColorScheme === "dark";
const routes = useRouteItems();
@ -111,23 +112,10 @@ const AppNavbar: FunctionComponent = () => {
}, [pathname]);
return (
<MantineNavbar
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],
},
})}
>
<AppShell.Navbar p="xs" className={styles.nav}>
<Selection.Provider value={{ selection, select }}>
<MantineNavbar.Section grow>
<Stack spacing={0}>
<AppShell.Section grow>
<Stack gap={0}>
{routes.map((route, idx) => (
<RouteItem
key={BuildKey("nav", idx)}
@ -136,10 +124,10 @@ const AppNavbar: FunctionComponent = () => {
></RouteItem>
))}
</Stack>
</MantineNavbar.Section>
</AppShell.Section>
<Divider></Divider>
<MantineNavbar.Section mt="xs">
<Group spacing="xs">
<AppShell.Section mt="xs">
<Group gap="xs">
<Action
label="Change Theme"
color={dark ? "yellow" : "indigo"}
@ -159,9 +147,9 @@ const AppNavbar: FunctionComponent = () => {
></Action>
</Anchor>
</Group>
</MantineNavbar.Section>
</AppShell.Section>
</Selection.Provider>
</MantineNavbar>
</AppShell.Navbar>
);
};
@ -186,7 +174,7 @@ const RouteItem: FunctionComponent<{
if (children !== undefined) {
const elements = (
<Stack spacing={0}>
<Stack gap={0}>
{children.map((child, idx) => (
<RouteItem
parent={link}
@ -199,7 +187,7 @@ const RouteItem: FunctionComponent<{
if (name) {
return (
<Stack spacing={0}>
<Stack gap={0}>
<NavbarItem
primary
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 {
name: string;
link: string;
@ -308,8 +249,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
onClick,
primary = false,
}) => {
const { classes } = useStyles();
const { show } = useNavbar();
const { ref, hovered } = useHover();
@ -335,9 +274,9 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
}}
className={({ isActive }) =>
clsx(
clsx(classes.anchor, {
[classes.active]: isActive,
[classes.hover]: hovered,
clsx(styles.anchor, {
[styles.active]: isActive,
[styles.hover]: hovered,
}),
)
}
@ -347,18 +286,19 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
inline
p="xs"
size="sm"
weight={primary ? "bold" : "normal"}
className={classes.text}
fw={primary ? "bold" : "normal"}
className={styles.text}
span
>
{icon && (
<FontAwesomeIcon
className={classes.icon}
className={styles.icon}
icon={icon}
></FontAwesomeIcon>
)}
{name}
{shouldHideBadge === false && (
<Badge className={classes.badge} radius="xs">
{!shouldHideBadge && (
<Badge className={styles.badge} radius="xs">
{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 { RouterNames } from "@/Router/RouterNames";
import ErrorBoundary from "@/components/ErrorBoundary";
import { Layout } from "@/constants";
import NavbarProvider from "@/contexts/Navbar";
import OnlineProvider from "@/contexts/Online";
import { notification } from "@/modules/task";
@ -13,6 +12,7 @@ import { showNotification } from "@mantine/notifications";
import { FunctionComponent, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom";
import AppHeader from "./Header";
import styleVars from "@/assets/_variables.module.scss";
const App: FunctionComponent = () => {
const navigate = useNavigate();
@ -55,13 +55,19 @@ const App: FunctionComponent = () => {
<NavbarProvider value={{ showed: navbar, show: setNavbar }}>
<OnlineProvider value={{ online, setOnline }}>
<AppShell
navbarOffsetBreakpoint={Layout.MOBILE_BREAKPOINT}
header={<AppHeader></AppHeader>}
navbar={<AppNavbar></AppNavbar>}
navbar={{
width: styleVars.navBarWidth,
breakpoint: "sm",
collapsed: { mobile: !navbar },
}}
header={{ height: { base: styleVars.headerHeight } }}
padding={0}
fixed
>
<Outlet></Outlet>
<AppHeader></AppHeader>
<AppNavbar></AppNavbar>
<AppShell.Main>
<Outlet></Outlet>
</AppShell.Main>
</AppShell>
</OnlineProvider>
</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 { 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"));
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 {
Anchor,
Autocomplete,
createStyles,
SelectItemProps,
ComboboxItem,
OptionsFilter,
} from "@mantine/core";
import { forwardRef, FunctionComponent, useMemo, useState } from "react";
import { FunctionComponent, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import styles from "./Search.module.scss";
type SearchResultItem = {
value: string;
@ -41,36 +42,35 @@ function useSearch(query: string) {
);
}
const useStyles = createStyles((theme) => {
return {
result: {
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();
const optionsFilter: OptionsFilter = ({ options, search }) => {
const lowercaseSearch = search.toLowerCase();
const trimmedSearch = search.trim();
return (options as ComboboxItem[]).filter((option) => {
return (
<Anchor
component={Link}
to={link}
underline={false}
className={styles.classes.result}
p="sm"
>
{value}
</Anchor>
option.value.toLowerCase().includes(lowercaseSearch) ||
option.value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.includes(trimmedSearch)
);
},
);
});
};
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 [query, setQuery] = useState("");
@ -79,22 +79,22 @@ const Search: FunctionComponent = () => {
return (
<Autocomplete
icon={<FontAwesomeIcon icon={faSearch} />}
itemComponent={ResultComponent}
leftSection={<FontAwesomeIcon icon={faSearch} />}
renderOption={(input) => (
<ResultComponent
name={input.option.value}
link={
results.find((a) => a.value === input.option.value)?.link || "/"
}
/>
)}
placeholder="Search"
size="sm"
data={results}
value={query}
onChange={setQuery}
onBlur={() => setQuery("")}
filter={(value, item) =>
item.value.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.includes(value.trim())
}
filter={optionsFilter}
></Autocomplete>
);
};

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

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

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

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

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

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

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

@ -1,7 +1,6 @@
import { useMovieSubtitleModification } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals";
import { TaskGroup, task } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form";
import {
@ -19,7 +18,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
Checkbox,
createStyles,
Divider,
MantineColor,
Stack,
@ -79,21 +77,12 @@ interface Props {
onComplete?: () => void;
}
const useStyles = createStyles((theme) => {
return {
wrapper: {
overflowWrap: "anywhere",
},
};
});
const MovieUploadForm: FunctionComponent<Props> = ({
files,
movie,
onComplete,
}) => {
const modals = useModals();
const { classes } = useStyles();
const profile = useLanguageProfileBy(movie.profileId);
@ -187,7 +176,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
return (
<TextPopover text={value?.messages}>
<Text color={color} inline>
<Text c={color} inline>
<FontAwesomeIcon icon={icon}></FontAwesomeIcon>
</Text>
</TextPopover>
@ -199,9 +188,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
id: "filename",
accessor: "file",
Cell: ({ value }) => {
const { classes } = useTableStyles();
return <Text className={classes.primary}>{value.name}</Text>;
return <Text className="table-primary">{value.name}</Text>;
},
},
{
@ -236,11 +223,10 @@ const MovieUploadForm: FunctionComponent<Props> = ({
Header: "Language",
accessor: "language",
Cell: ({ row: { original, index }, value }) => {
const { classes } = useTableStyles();
return (
<Selector
{...languageOptions}
className={classes.select}
className="table-long-break"
value={value}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
@ -289,7 +275,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
modals.closeSelf();
})}
>
<Stack className={classes.wrapper}>
<Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider>
<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 { useModals, withModal } from "@/modules/modals";
import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities";
import { LOG } from "@/utilities/console";
import FormUtils from "@/utilities/form";
@ -19,6 +18,7 @@ import { useForm } from "@mantine/form";
import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table";
import ChipInput from "../inputs/ChipInput";
import styles from "./ProfileEditForm.module.scss";
export const anyCutoff = 65535;
@ -166,12 +166,10 @@ const ProfileEditForm: FunctionComponent<Props> = ({
[code],
);
const { classes } = useTableStyles();
return (
<Selector
{...languageOptions}
className={classes.select}
className="table-select"
value={language}
onChange={(value) => {
if (value) {
@ -262,13 +260,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
multiple
chevronPosition="right"
defaultValue={["Languages"]}
styles={(theme) => ({
content: {
[theme.fn.smallerThan("md")]: {
padding: 0,
},
},
})}
className={styles.content}
>
<Accordion.Item value="Languages">
<Stack>
@ -277,7 +269,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
columns={columns}
data={form.values.items}
></SimpleTable>
<Button fullWidth color="light" onClick={addItem}>
<Button fullWidth onClick={addItem}>
Add Language
</Button>
<Selector

@ -5,7 +5,6 @@ import {
} from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form";
import {
@ -23,7 +22,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
Checkbox,
createStyles,
Divider,
MantineColor,
Stack,
@ -86,21 +84,12 @@ interface Props {
onComplete?: VoidFunction;
}
const useStyles = createStyles((theme) => {
return {
wrapper: {
overflowWrap: "anywhere",
},
};
});
const SeriesUploadForm: FunctionComponent<Props> = ({
series,
files,
onComplete,
}) => {
const modals = useModals();
const { classes } = useStyles();
const episodes = useEpisodesBySeriesId(series.sonarrSeriesId);
const episodeOptions = useSelectorOptions(
episodes.data ?? [],
@ -225,8 +214,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
id: "filename",
accessor: "file",
Cell: ({ value: { name } }) => {
const { classes } = useTableStyles();
return <Text className={classes.primary}>{name}</Text>;
return <Text className="table-primary">{name}</Text>;
},
},
{
@ -283,11 +271,10 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
),
accessor: "language",
Cell: ({ row: { original, index }, value }) => {
const { classes } = useTableStyles();
return (
<Selector
{...languageOptions}
className={classes.select}
className="table-select"
value={value}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
@ -301,12 +288,11 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
Header: "Episode",
accessor: "episode",
Cell: ({ value, row }) => {
const { classes } = useTableStyles();
return (
<Selector
{...episodeOptions}
searchable
className={classes.select}
className="table-select"
value={value}
onChange={(item) => {
action.mutate(row.index, { ...row.original, episode: item });
@ -368,7 +354,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
modals.closeSelf();
})}
>
<Stack className={classes.wrapper}>
<Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider>
<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 { useForm } from "@mantine/form";
import { FunctionComponent } from "react";
import { Selector, SelectorOption } from "../inputs";
import { GroupedSelector, Selector } from "../inputs";
const TaskName = "Syncing Subtitle";
interface SelectOptions {
group: string;
items: { value: string; label: string }[];
}
function useReferencedSubtitles(
mediaType: "episode" | "movie",
mediaId: number,
@ -37,15 +42,21 @@ function useReferencedSubtitles(
const mediaData = mediaType === "episode" ? episodeData : movieData;
const subtitles: { group: string; value: string; label: string }[] = [];
const subtitles: SelectOptions[] = [];
if (!mediaData.data) {
return [];
} else {
if (mediaData.data.audio_tracks.length > 0) {
const embeddedAudioGroup: SelectOptions = {
group: "Embedded audio tracks",
items: [],
};
subtitles.push(embeddedAudioGroup);
mediaData.data.audio_tracks.forEach((item) => {
subtitles.push({
group: "Embedded audio tracks",
embeddedAudioGroup.items.push({
value: item.stream,
label: `${item.name || item.language} (${item.stream})`,
});
@ -53,9 +64,15 @@ function useReferencedSubtitles(
}
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) => {
subtitles.push({
group: "Embedded subtitles tracks",
embeddedSubtitlesTrackGroup.items.push({
value: item.stream,
label: `${item.name || item.language} (${item.stream})`,
});
@ -63,10 +80,16 @@ function useReferencedSubtitles(
}
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) => {
if (item) {
subtitles.push({
group: "External Subtitles files",
externalSubtitlesFilesGroup.items.push({
value: item.path,
label: item.name,
});
@ -105,7 +128,7 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
const mediaId = selections[0].id;
const subtitlesPath = selections[0].path;
const subtitles: SelectorOption<string>[] = useReferencedSubtitles(
const subtitles: SelectOptions[] = useReferencedSubtitles(
mediaType,
mediaId,
subtitlesPath,
@ -145,14 +168,14 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
>
<Text size="sm">{selections.length} subtitles selected</Text>
</Alert>
<Selector
<GroupedSelector
clearable
disabled={subtitles.length === 0 || selections.length !== 1}
label="Reference"
placeholder="Default: choose automatically within video file"
options={subtitles}
{...form.getInputProps("reference")}
></Selector>
></GroupedSelector>
<Selector
clearable
label="Max Offset Seconds"

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

@ -1,4 +1,4 @@
export { default as Search } from "./Search";
export * from "./inputs";
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 userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
@ -9,7 +9,7 @@ const testIcon = faStickyNote;
describe("Action 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 });
expect(element.getAttribute("type")).toEqual("button");
@ -17,7 +17,7 @@ describe("Action button", () => {
});
it("should show icon", () => {
rawRender(<Action icon={testIcon} label={testLabel}></Action>);
render(<Action icon={testIcon} label={testLabel}></Action>);
// TODO: use getBy...
const element = screen.getByRole("img", { hidden: true });
@ -27,7 +27,7 @@ describe("Action button", () => {
it("should call on-click event when clicked", async () => {
const onClickFn = vitest.fn();
rawRender(
render(
<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 { describe, it, vitest } from "vitest";
import ChipInput from "./ChipInput";
@ -8,7 +8,7 @@ describe("ChipInput", () => {
// TODO: Support default value
it.skip("should works with default value", () => {
rawRender(<ChipInput defaultValue={existedValues}></ChipInput>);
render(<ChipInput defaultValue={existedValues}></ChipInput>);
existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined();
@ -16,7 +16,7 @@ describe("ChipInput", () => {
});
it("should works with value", () => {
rawRender(<ChipInput value={existedValues}></ChipInput>);
render(<ChipInput value={existedValues}></ChipInput>);
existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined();
@ -29,9 +29,7 @@ describe("ChipInput", () => {
expect(values).toContain(typedValue);
});
rawRender(
<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>,
);
render(<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>);
const element = screen.getByRole("searchbox");

@ -1,35 +1,29 @@
import { useSelectorOptions } from "@/utilities";
import { FunctionComponent } from "react";
import { MultiSelector, MultiSelectorProps } from "./Selector";
import { TagsInput } from "@mantine/core";
export type ChipInputProps = Omit<
MultiSelectorProps<string>,
| "searchable"
| "creatable"
| "getCreateLabel"
| "onCreate"
| "options"
| "getkey"
>;
const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
const { value, onChange } = props;
const options = useSelectorOptions(value ?? [], (v) => v);
export interface ChipInputProps {
defaultValue?: string[] | undefined;
value?: readonly string[] | null;
label?: string;
onChange?: (value: string[]) => void;
}
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 (
<MultiSelector
{...props}
{...options}
creatable
searchable
getCreateLabel={(query) => `Add "${query}"`}
onCreate={(query) => {
onChange?.([...(value ?? []), query]);
return query;
}}
buildOption={(value) => value}
></MultiSelector>
<TagsInput
defaultValue={defaultValue}
label={label}
value={value ? value?.map((v) => v) : []}
onChange={onChange}
clearable
></TagsInput>
);
};

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

@ -4,24 +4,14 @@ import {
faXmark,
} from "@fortawesome/free-solid-svg-icons";
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 { FunctionComponent } from "react";
const useStyle = createStyles((theme) => {
return {
container: {
pointerEvents: "none",
minHeight: 220,
},
};
});
import styles from "./DropContent.module.scss";
export const DropContent: FunctionComponent = () => {
const { classes } = useStyle();
return (
<Group position="center" spacing="xl" className={classes.container}>
<Group justify="center" gap="xl" className={styles.container}>
<Dropzone.Idle>
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
</Dropzone.Idle>
@ -31,9 +21,9 @@ export const DropContent: FunctionComponent = () => {
<Dropzone.Reject>
<FontAwesomeIcon icon={faXmark} size="2x" />
</Dropzone.Reject>
<Stack spacing={0}>
<Stack gap={0}>
<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
metadata before uploading
</Text>

@ -1,7 +1,12 @@
import { useFileSystem } from "@/apis/hooks";
import { faFolder } from "@fortawesome/free-regular-svg-icons";
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";
// TODO: use fortawesome icons
@ -75,24 +80,28 @@ export const FileBrowser: FunctionComponent<FileBrowserProps> = ({
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 (
<Autocomplete
{...props}
ref={ref}
icon={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
leftSection={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
placeholder="Click to start"
data={data}
value={value}
// Temporary solution of infinite dropdown items, fix later
limit={NaN}
maxDropdownHeight={240}
filter={(value, item) => {
if (item.value === backKey) {
return true;
} else {
return item.value.includes(value);
}
}}
filter={optionsFilter}
onChange={(val) => {
if (val !== backKey) {
setValue(val);

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

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

@ -1,6 +1,5 @@
import { withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
import { useTableStyles } from "@/styles";
import { GetItemId } from "@/utilities";
import {
faCaretDown,
@ -31,9 +30,7 @@ type SupportType = Item.Movie | Item.Episode;
interface Props<T extends SupportType> {
download: (item: T, result: SearchResultType) => Promise<void>;
query: (
id?: number,
) => UseQueryResult<SearchResultType[] | undefined, unknown>;
query: (id?: number) => UseQueryResult<SearchResultType[] | undefined>;
item: T;
}
@ -50,7 +47,8 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
const search = useCallback(() => {
setSearchStarted(true);
results.refetch();
void results.refetch();
}, [results]);
const columns = useMemo<Column<SearchResultType>[]>(
@ -59,8 +57,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Score",
accessor: "score",
Cell: ({ value }) => {
const { classes } = useTableStyles();
return <Text className={classes.noWrap}>{value}%</Text>;
return <Text className="table-no-wrap">{value}%</Text>;
},
},
{
@ -84,13 +81,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Provider",
accessor: "provider",
Cell: (row) => {
const { classes } = useTableStyles();
const value = row.value;
const { url } = row.row.original;
if (url) {
return (
<Anchor
className={classes.noWrap}
className="table-no-wrap"
href={url}
target="_blank"
rel="noopener noreferrer"
@ -107,7 +103,6 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Release",
accessor: "release_info",
Cell: ({ value }) => {
const { classes } = useTableStyles();
const [open, setOpen] = useState(false);
const items = useMemo(
@ -116,12 +111,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
);
if (value.length === 0) {
return <Text color="dimmed">Cannot get release info</Text>;
return <Text c="dimmed">Cannot get release info</Text>;
}
return (
<Stack spacing={0} onClick={() => setOpen((o) => !o)}>
<Text className={classes.primary}>
<Stack gap={0} onClick={() => setOpen((o) => !o)}>
<Text className="table-primary" span>
{value[0]}
{value.length > 1 && (
<FontAwesomeIcon
@ -141,8 +136,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
Header: "Uploader",
accessor: "uploader",
Cell: ({ value }) => {
const { classes } = useTableStyles();
return <Text className={classes.noWrap}>{value ?? "-"}</Text>;
return <Text className="table-no-wrap">{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 { 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 { HeaderGroup, Row, TableInstance } from "react-table";
import styles from "./BaseTable.module.scss";
export type BaseTableProps<T extends object> = TableInstance<T> & {
tableStyles?: TableStyleProps<T>;
@ -18,37 +19,23 @@ export interface TableStyleProps<T extends object> {
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>(
headers: HeaderGroup<T>[],
): JSX.Element[] {
return headers.map((col) => (
<th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
<Table.Th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
{col.render("Header")}
</th>
</Table.Th>
));
}
function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null {
return (
<tr {...row.getRowProps()}>
<Table.Tr {...row.getRowProps()}>
{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 rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
const { classes } = useStyles();
const colCount = useMemo(() => {
return headerGroups.reduce(
(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)
.fill(0)
.map((_, i) => (
<tr key={i}>
<td colSpan={colCount}>
<Table.Tr key={i}>
<Table.Td colSpan={colCount}>
<Skeleton height={24}></Skeleton>
</td>
</tr>
</Table.Td>
</Table.Tr>
));
} else if (empty && tableStyles?.emptyText) {
body = (
<tr>
<td colSpan={colCount}>
<Text align="center">{tableStyles.emptyText}</Text>
</td>
</tr>
<Table.Tr>
<Table.Td colSpan={colCount}>
<Text ta="center">{tableStyles.emptyText}</Text>
</Table.Td>
</Table.Tr>
);
} else {
body = rows.map((row) => {
@ -110,20 +95,20 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
}
return (
<Box className={classes.container}>
<Box className={styles.container}>
<Table
className={classes.table}
className={styles.table}
striped={tableStyles?.striped ?? true}
{...getTableProps()}
>
<thead className={classes.header} hidden={tableStyles?.hideHeader}>
<Table.Thead hidden={tableStyles?.hideHeader}>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
<Table.Tr {...headerGroup.getHeaderGroupProps()}>
{headersRenderer(headerGroup.headers)}
</tr>
</Table.Tr>
))}
</thead>
<tbody {...getTableBodyProps()}>{body}</tbody>
</Table.Thead>
<Table.Tbody {...getTableBodyProps()}>{body}</Table.Tbody>
</Table>
</Box>
);

@ -1,6 +1,6 @@
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Box, Text } from "@mantine/core";
import { Box, Text, Table } from "@mantine/core";
import {
Cell,
HeaderGroup,
@ -29,8 +29,8 @@ function renderRow<T extends object>(row: Row<T>) {
if (cell) {
const rotation = row.isExpanded ? 90 : undefined;
return (
<tr {...row.getRowProps()}>
<td {...cell.getCellProps()} colSpan={row.cells.length}>
<Table.Tr {...row.getRowProps()}>
<Table.Td {...cell.getCellProps()} colSpan={row.cells.length}>
<Text {...row.getToggleRowExpandedProps()} p={2}>
{cell.render("Cell")}
<Box component="span" mx={12}>
@ -40,21 +40,23 @@ function renderRow<T extends object>(row: Row<T>) {
></FontAwesomeIcon>
</Box>
</Text>
</td>
</tr>
</Table.Td>
</Table.Tr>
);
} else {
return null;
}
} else {
return (
<tr {...row.getRowProps()}>
<Table.Tr {...row.getRowProps()}>
{row.cells
.filter((cell) => !cell.isPlaceholder)
.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[] {
return headers
.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<

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

@ -24,7 +24,7 @@ const ToolboxButton: FunctionComponent<ToolboxButtonProps> = ({
<Button
color="dark"
variant="subtle"
leftIcon={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
{...props}
>
<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 ToolboxButton, { ToolboxMutateButton } from "./Button";
const useStyles = createStyles((theme) => ({
group: {
backgroundColor:
theme.colorScheme === "light"
? theme.colors.gray[3]
: theme.colors.dark[5],
},
}));
import styles from "./Toolbox.module.scss";
declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
Button: typeof ToolboxButton;
@ -17,9 +9,8 @@ declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
};
const Toolbox: ToolboxComp = ({ children }) => {
const { classes } = useStyles();
return (
<Group p={12} position="apart" className={classes.group}>
<Group p={12} justify="apart" className={styles.group}>
{children}
</Group>
);

@ -1,9 +1 @@
import { MantineNumberSize } from "@mantine/core";
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) => {
msg
.map((message) => notification.info("Notification", message))
.forEach(showNotification);
.forEach((data) => showNotification(data));
},
},
{

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

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

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

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

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

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

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

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

@ -9,7 +9,6 @@ import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon";
import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView";
import { useTableStyles } from "@/styles";
import {
faFileExcel,
faInfoCircle,
@ -32,11 +31,10 @@ const SeriesHistoryView: FunctionComponent = () => {
Header: "Series",
accessor: "seriesTitle",
Cell: (row) => {
const { classes } = useTableStyles();
const target = `/series/${row.row.original.sonarrSeriesId}`;
return (
<Anchor className={classes.primary} component={Link} to={target}>
<Anchor className="table-primary" component={Link} to={target}>
{row.value}
</Anchor>
);
@ -50,8 +48,7 @@ const SeriesHistoryView: FunctionComponent = () => {
Header: "Title",
accessor: "episodeTitle",
Cell: ({ value }) => {
const { classes } = useTableStyles();
return <Text className={classes.noWrap}>{value}</Text>;
return <Text className="table-no-wrap">{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";
import { Selector, Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
import Language from "@/components/bazarr/Language";
import { Layout } from "@/constants";
import { useSelectorOptions } from "@/utilities";
import {
Box,
Container,
SimpleGrid,
createStyles,
useMantineTheme,
} from "@mantine/core";
import { Box, Container, SimpleGrid, useMantineTheme } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { merge } from "lodash";
import { FunctionComponent, useMemo, useState } from "react";
@ -29,17 +21,7 @@ import {
YAxis,
} from "recharts";
import { actionOptions, timeFrameOptions } from "./options";
const useStyles = createStyles((theme) => ({
container: {
display: "flex",
flexDirection: "column",
height: `calc(100vh - ${Layout.HEADER_HEIGHT}px)`,
},
chart: {
height: "90%",
},
}));
import styles from "./HistoryStats.module.scss";
const HistoryStats: FunctionComponent = () => {
const { data: providers } = useSystemProviders(true);
@ -71,8 +53,8 @@ const HistoryStats: FunctionComponent = () => {
date: v.date,
series: v.count,
}));
const result = merge(movies, series);
return result;
return merge(movies, series);
} else {
return [];
}
@ -80,20 +62,13 @@ const HistoryStats: FunctionComponent = () => {
useDocumentTitle("History Statistics - Bazarr");
const { classes } = useStyles();
const theme = useMantineTheme();
return (
<Container fluid px={0} className={classes.container}>
<Container fluid px={0} className={styles.container}>
<QueryOverlay result={stats}>
<Toolbox>
<SimpleGrid
cols={4}
breakpoints={[
{ maxWidth: "sm", cols: 4 },
{ maxWidth: "xs", cols: 2 },
]}
>
<SimpleGrid cols={{ base: 4, xs: 2 }}>
<Selector
placeholder="Time..."
options={timeFrameOptions}
@ -123,9 +98,9 @@ const HistoryStats: FunctionComponent = () => {
></Selector>
</SimpleGrid>
</Toolbox>
<Box className={classes.chart} m="xs">
<Box className={styles.chart} m="xs">
<ResponsiveContainer>
<BarChart className={classes.chart} data={convertedData}>
<BarChart className={styles.chart} data={convertedData}>
<CartesianGrid strokeDasharray="4 2"></CartesianGrid>
<XAxis dataKey="date"></XAxis>
<YAxis allowDecimals={false}></YAxis>

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

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

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

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

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

@ -4,7 +4,7 @@ import {
faClipboard,
faSync,
} 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 { FunctionComponent, useState } from "react";
import {
@ -54,7 +54,7 @@ const SettingsGeneralView: FunctionComponent = () => {
></Number>
<Text
label="Base URL"
icon="/"
leftSection="/"
settingKey="settings-general-base_url"
settingOptions={{
onLoaded: (s) => s.general.base_url?.slice(1) ?? "",
@ -87,7 +87,7 @@ const SettingsGeneralView: FunctionComponent = () => {
rightSectionWidth={95}
rightSectionProps={{ style: { justifyContent: "flex-end" } }}
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
window.isSecureContext && (
@ -204,13 +204,12 @@ const SettingsGeneralView: FunctionComponent = () => {
<Number
label="Retention"
settingKey="settings-backup-retention"
styles={{
rightSection: { width: "4rem", justifyContent: "flex-end" },
}}
rightSection={
<MantineText size="xs" px="sm" color="dimmed">
Days
</MantineText>
<Box w="4rem" style={{ justifyContent: "flex-end" }}>
<MantineText size="xs" px="sm" c="dimmed">
Days
</MantineText>
</Box>
}
></Number>
</Section>

@ -355,7 +355,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
return (
<>
<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"}
</Button>
</>

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

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

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

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

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

@ -501,7 +501,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
label="Command"
settingKey="settings-general-postprocessing_cmd"
></Text>
<Table highlightOnHover fontSize="sm">
<Table highlightOnHover fs="sm">
<tbody>{commandOptionElements}</tbody>
</Table>
</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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Center,
createStyles,
Stack,
Text,
UnstyledButton,
} from "@mantine/core";
import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
import { FunctionComponent } from "react";
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%",
},
};
});
import styles from "./Card.module.scss";
interface CardProps {
header?: string;
@ -39,16 +17,15 @@ export const Card: FunctionComponent<CardProps> = ({
plus,
onClick,
}) => {
const { classes } = useCardStyles();
return (
<UnstyledButton p="lg" onClick={onClick} className={classes.card}>
<UnstyledButton p="lg" onClick={onClick} className={styles.card}>
{plus ? (
<Center>
<FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon>
</Center>
) : (
<Stack className={classes.stack} spacing={0} align="flex-start">
<Text weight="bold">{header}</Text>
<Stack h="100%" gap={0} align="flex-start">
<Text fw="bold">{header}</Text>
<Text hidden={description === undefined}>{description}</Text>
</Stack>
)}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save