🔀 Merge branch 'dev' into tests/add-tests

This commit is contained in:
Manuel
2023-03-29 12:17:20 +02:00
50 changed files with 1035 additions and 910 deletions

View File

@@ -57,7 +57,12 @@ export default function ConfigChanger() {
size="lg"
radius="md"
>
<Notification loading title={t('configSelect.loadingNew')} radius="md" disallowClose>
<Notification
loading
title={t('configSelect.loadingNew')}
radius="md"
withCloseButton={false}
>
{t('configSelect.pleaseWait')}
</Notification>
</Dialog>

View File

@@ -1,18 +1,19 @@
import {
Accordion,
ActionIcon,
Anchor,
Badge,
Button,
createStyles,
Divider,
Grid,
Group,
HoverCard,
Kbd,
Modal,
Stack,
Table,
Text,
Title,
Tooltip,
} from '@mantine/core';
import {
IconAnchor,
@@ -35,6 +36,8 @@ import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
import { useColorTheme } from '../../../../tools/color';
import Tip from '../../../layout/Tip';
import { usePrimaryGradient } from '../../../layout/useGradient';
import Credits from '../../../Settings/Common/Credits';
@@ -50,6 +53,23 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
const informations = useInformationTableItems(newVersionAvailable);
const { t } = useTranslation(['common', 'layout/modals/about']);
const keybinds = [
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' },
{ key: 'Mod + K', shortcut: 'Focus on search bar' },
{ key: 'Mod + B', shortcut: 'Open docker widget' },
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' },
];
const rows = keybinds.map((element) => (
<tr key={element.key}>
<td>
<Kbd>{element.key}</Kbd>
</td>
<td>
<Text>{element.shortcut}</Text>
</td>
</tr>
));
return (
<Modal
onClose={() => closeModal()}
@@ -76,7 +96,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
<Trans i18nKey="layout/modals/about:description" />
</Text>
<Table mb="lg" striped highlightOnHover withBorder>
<Table mb="lg" highlightOnHover withBorder>
<tbody>
{informations.map((item, index) => (
<tr key={index}>
@@ -100,8 +120,26 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
))}
</tbody>
</Table>
<Accordion mb={5} variant="contained" radius="md">
<Accordion.Item value="keybinds">
<Accordion.Control icon={<IconKey size={20} />}>
{t('layout/modals/about:keybinds')}
</Accordion.Control>
<Accordion.Panel>
<Table mb={5}>
<thead>
<tr>
<th>{t('layout/modals/about:key')}</th>
<th>{t('layout/modals/about:action')}</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
<Tip>{t('layout/modals/about:tip')}</Tip>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
<Divider variant="dashed" mb="md" />
<Title order={6} mb="xs" align="center">
{t('layout/modals/about:contact')}
</Title>
@@ -161,9 +199,9 @@ interface ExtendedInitOptions extends InitOptions {
}
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
const colorGradiant = usePrimaryGradient();
const { attributes } = usePackageAttributesStore();
const { editModeEnabled } = useEditModeInformationStore();
const { primaryColor } = useColorTheme();
const { configVersion } = useConfigContext();
const { configs } = useConfigStore();
@@ -177,15 +215,19 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconKey size={20} />,
label: 'experimental_disableEditMode',
content: (
<Stack>
<Tooltip
color="red"
withinPortal
width={300}
multiline
withArrow
label="This is an experimental feature, where the edit mode is disabled entirely - no config
modifications are possbile anymore. All update requests for the config will be dropped
on the API. This will be removed in future versions, as Homarr will receive a proper
authentication system, which will make this obsolete."
>
<Badge color="red">WARNING</Badge>
<Text color="red" size="xs">
This is an experimental feature, where the edit mode is disabled entirely - no config
modifications are possbile anymore. All update requests for the config will be dropped
on the API. This will be removed in future versions, as Homarr will receive a proper
authentication system, which will make this obsolete.
</Text>
</Stack>
</Tooltip>
),
},
];
@@ -201,7 +243,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconLanguage size={20} />,
label: 'i18n',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
<Badge variant="light" color={primaryColor}>
{usedI18nNamespaces.length}
</Badge>
),
@@ -210,7 +252,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconVocabulary size={20} />,
label: 'locales',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
<Badge variant="light" color={primaryColor}>
{initOptions.locales.length}
</Badge>
),
@@ -223,7 +265,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconSchema size={20} />,
label: 'configurationSchemaVersion',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
<Badge variant="light" color={primaryColor}>
{configVersion}
</Badge>
),
@@ -232,7 +274,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconFile size={20} />,
label: 'configurationsCount',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
<Badge variant="light" color={primaryColor}>
{configs.length}
</Badge>
),
@@ -242,7 +284,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
label: 'version',
content: (
<Group position="right">
<Badge variant="gradient" gradient={colorGradiant}>
<Badge variant="light" color={primaryColor}>
{attributes.packageVersion ?? 'Unknown'}
</Badge>
{newVersionAvailable && (
@@ -282,7 +324,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconAnchor size={20} />,
label: 'nodeEnvironment',
content: (
<Badge variant="gradient" gradient={colorGradiant}>
<Badge variant="light" color={primaryColor}>
{attributes.environment}
</Badge>
),

View File

@@ -6,7 +6,6 @@ import {
Grid,
Group,
PasswordInput,
Stack,
ThemeIcon,
Title,
Text,
@@ -40,7 +39,7 @@ export const GenericSecretInput = ({
const Icon = setIcon;
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(!secretIsPresent);
const { t } = useTranslation(['layout/modals/add-app', 'common']);
return (
@@ -51,26 +50,26 @@ export const GenericSecretInput = ({
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
<Icon size={18} />
</ThemeIcon>
<Stack spacing={0}>
<Flex justify="start" align="start" direction="column">
<Group spacing="xs">
<Title className={classes.subtitle} order={6}>
{t(label)}
</Title>
<Group spacing="xs">
{secretIsPresent ? (
<Badge className={classes.textTransformUnset} color="green" variant="dot">
{t('integration.type.defined')}
</Badge>
) : (
<Badge className={classes.textTransformUnset} color="red" variant="dot">
{t('integration.type.undefined')}
</Badge>
)}
<Badge
className={classes.textTransformUnset}
color={secretIsPresent ? 'green' : 'red'}
variant="dot"
>
{secretIsPresent
? t('integration.type.defined')
: t('integration.type.undefined')}
</Badge>
{type === 'private' ? (
<Tooltip
label={t('integration.type.explanationPrivate')}
width={200}
width={400}
multiline
withinPortal
withArrow
@@ -82,7 +81,7 @@ export const GenericSecretInput = ({
) : (
<Tooltip
label={t('integration.type.explanationPublic')}
width={200}
width={400}
multiline
withinPortal
withArrow
@@ -94,29 +93,20 @@ export const GenericSecretInput = ({
)}
</Group>
</Group>
<Text size="xs" color="dimmed">
<Text size="xs" color="dimmed" w={400}>
{type === 'private'
? 'Private: Once saved, you cannot read out this value again'
: 'Public: Can be read out repeatedly'}
</Text>
</Stack>
</Flex>
</Group>
</Grid.Col>
<Grid.Col xs={12} md={6}>
<Flex gap={10} justify="end" align="end">
<Button
onClick={() => {
setDisplayUpdateField(false);
onClickUpdateButton(undefined);
}}
variant="subtle"
color="gray"
px="xl"
>
{t('integration.secrets.clear')}
</Button>
{displayUpdateField === true ? (
<PasswordInput
required
defaultValue={value}
placeholder="new secret"
styles={{ root: { width: 200 } }}
{...props}

View File

@@ -29,7 +29,7 @@ export const GenericAvailableElementType = ({
: image;
return (
<Grid.Col span={3}>
<Grid.Col span="auto">
<Card style={{ height: '100%' }}>
<Stack justify="space-between" style={{ height: '100%' }}>
<Stack spacing="xs">

View File

@@ -19,7 +19,7 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
they don&apos;t integrate with any apps and their content never changes.
</Text>
<Grid>
<Grid grow>
<GenericAvailableElementType
name="Static Text"
description="Display a fixed string on your dashboard"

View File

@@ -93,12 +93,10 @@ export const AppTile = ({ className, app }: AppTileProps) => {
const useStyles = createStyles((theme, _params, getRef) => ({
image: {
ref: getRef('image'),
maxHeight: '90%',
maxWidth: '90%',
},
appName: {
ref: getRef('appName'),
wordBreak: 'break-word',
},
button: {

View File

@@ -18,12 +18,11 @@ interface GridstackStoreType {
export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | null => {
const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth);
const { sm, xl } = useMantineTheme().breakpoints;
if (!mainAreaWidth) return null;
if (mainAreaWidth >= xl) return 'large';
if (mainAreaWidth >= 1400) return 'large';
if (mainAreaWidth >= sm) return 'medium';
if (mainAreaWidth >= 800) return 'medium';
return 'small';
};

View File

@@ -18,7 +18,7 @@ export default function CommonSettings() {
);
}
return (
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
<Stack>
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
<Space />

View File

@@ -60,6 +60,7 @@ export const SearchEngineSelector = ({ searchEngine }: Props) => {
<Space mb="md" />
<TextInput
label={t('customEngine.label')}
name={t('configurationName')}
description={t('tips.placeholderTip')}
placeholder={t('customEngine.placeholder')}
value={searchUrl}

View File

@@ -8,7 +8,7 @@ export default function CustomizationSettings() {
const { t } = useTranslation('settings/customization/general');
return (
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
<Stack mt="xs" mb="md" spacing="xs">
<Text color="dimmed">{t('text')}</Text>
<CustomizationSettingsAccordeon />

View File

@@ -41,7 +41,7 @@ export function SettingsDrawer({
return (
<Drawer
size="xl"
size="lg"
padding="lg"
position="right"
title={<Title order={5}>{t('title')}</Title>}

View File

@@ -29,7 +29,7 @@ export const ToggleEditModeAction = () => {
const { config } = useConfigContext();
const { classes } = useCardStyles(true);
useHotkeys([['ctrl+E', toggleEditMode]]);
useHotkeys([['mod+E', toggleEditMode]]);
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
if (enabled) {

View File

@@ -42,7 +42,7 @@ export function Header(props: any) {
>
<Search />
{!editModeEnabled && <ToggleEditModeAction />}
<DockerMenuButton />
{!editModeEnabled && <DockerMenuButton />}
<Indicator
size={15}
color="blue"

View File

@@ -181,7 +181,6 @@ export function Search() {
shadow="md"
radius="md"
zIndex={100}
transition="pop-top-right"
>
<Popover.Target>
<Autocomplete
@@ -297,7 +296,7 @@ export function Search() {
setSearchEngine(item);
showNotification({
radius: 'lg',
disallowClose: true,
withCloseButton: false,
id: 'spotlight',
autoClose: 1000,
icon: <ActionIcon size="sm">{item.icon}</ActionIcon>,

View File

@@ -7,6 +7,7 @@ import { AboutModal } from '../../Dashboard/Modals/AboutModal/AboutModal';
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
import { useCardStyles } from '../useCardStyles';
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
import { EditModeToggle } from './SettingsMenu/EditModeToggle';
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
const [drawerOpened, drawer] = useDisclosure(false);
@@ -25,6 +26,7 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
</Menu.Target>
<Menu.Dropdown>
<ColorSchemeSwitch />
<EditModeToggle />
<Menu.Divider />
{!editModeEnabled && (
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>

View File

@@ -0,0 +1,78 @@
import { Button, Code, Menu, PasswordInput, Stack, Text } from '@mantine/core';
import { useForm } from '@mantine/form';
import { openModal } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { IconEdit, IconEditOff } from '@tabler/icons';
import axios from 'axios';
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
function ModalContent() {
const form = useForm({
initialValues: {
triedPassword: '',
},
});
return (
<form
onSubmit={form.onSubmit((values) => {
axios
.post('/api/configs/tryPassword', { tried: values.triedPassword, type: 'edit' })
.then((res) => {
showNotification({
title: 'Success',
message: 'Successfully toggled edit mode, reloading the page...',
color: 'green',
});
setTimeout(() => {
window.location.reload();
}, 500);
})
.catch((_) => {
showNotification({
title: 'Error',
message: 'Failed to toggle edit mode, please try again.',
color: 'red',
});
});
})}
>
<Stack>
<Text size="sm">
In order to toggle edit mode, you need to enter the password you entered in the
environment variable named <Code>EDIT_MODE_PASSWORD</Code> . If it is not set, you are not
able to toggle edit mode on and off.
</Text>
<PasswordInput
id="triedPassword"
label="Edit password"
autoFocus
required
{...form.getInputProps('triedPassword')}
/>
<Button type="submit">Submit</Button>
</Stack>
</form>
);
}
export function EditModeToggle() {
const { editModeEnabled } = useEditModeInformationStore();
const Icon = editModeEnabled ? IconEdit : IconEditOff;
return (
<Menu.Item
closeMenuOnClick={false}
icon={<Icon strokeWidth={1.2} size={18} />}
onClick={() =>
openModal({
title: 'Toggle edit mode',
centered: true,
size: 'lg',
children: <ModalContent />,
})
}
>
{editModeEnabled ? 'Enable edit mode' : 'Disable edit mode'}
</Menu.Item>
);
}

View File

@@ -16,8 +16,8 @@ const ConfigContext = createContext<ConfigContextType>({
name: 'unknown',
config: undefined,
configVersion: undefined,
increaseVersion: () => console.error('Provider not set'),
setConfigName: () => console.error('Provider not set'),
increaseVersion: () => {},
setConfigName: () => {},
});
export const ConfigProvider = ({ children }: { children: ReactNode }) => {

View File

@@ -0,0 +1 @@
export const MIN_WIDTH_MOBILE = 500;

View File

@@ -1,8 +1,9 @@
import { MantineSize, useMantineTheme } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { MIN_WIDTH_MOBILE } from '../constants/constants';
export const useScreenLargerThan = (size: MantineSize | number) => {
const { breakpoints } = useMantineTheme();
const pixelCount = typeof size === 'string' ? breakpoints[size] : size;
return useMediaQuery(`(min-width: ${pixelCount}px)`);
return useMediaQuery(`(min-width: ${MIN_WIDTH_MOBILE})`);
};

View File

@@ -35,7 +35,7 @@ function sendDockerCommand(
title: `${t(`actions.${action}.start`)} ${containerName}`,
message: undefined,
autoClose: false,
disallowClose: true,
withCloseButton: false,
});
axios
.get(`/api/docker/container/${containerId}?action=${action}`)

View File

@@ -1,4 +1,4 @@
import { Badge, BadgeVariant, MantineSize } from '@mantine/core';
import { Badge, BadgeProps, MantineSize } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import Dockerode from 'dockerode';
@@ -14,7 +14,7 @@ export default function ContainerState(props: ContainerStateProps) {
const options: {
size: MantineSize;
radius: MantineSize;
variant: BadgeVariant;
variant: BadgeProps['variant'];
} = {
size: 'md',
radius: 'md',

View File

@@ -54,7 +54,7 @@ export default function DockerMenuButton(props: any) {
}, 300);
}
if (!dockerEnabled) {
if (!dockerEnabled || process.env.DISABLE_EDIT_MODE === 'true') {
return null;
}
@@ -66,10 +66,13 @@ export default function DockerMenuButton(props: any) {
onClose={() => setOpened(false)}
padding="xl"
position="right"
size="full"
size="100%"
title={<ContainerActionBar selected={selection} reload={reload} />}
transitionProps={{
transition: 'pop',
}}
styles={{
drawer: {
content: {
display: 'flex',
flexDirection: 'column',
},

View File

@@ -14,6 +14,7 @@ import { IconSearch } from '@tabler/icons';
import Dockerode from 'dockerode';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
import ContainerState from './ContainerState';
const useStyles = createStyles((theme) => ({
@@ -34,7 +35,6 @@ export default function DockerTable({
containers: Dockerode.ContainerInfo[];
selection: Dockerode.ContainerInfo[];
}) {
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
const [usedContainers, setContainers] = useState<Dockerode.ContainerInfo[]>(containers);
const { classes, cx } = useStyles();
const [search, setSearch] = useState('');

View File

@@ -221,7 +221,7 @@ function askForMedia(type: MediaType, id: number, name: string, seasons?: number
color: 'orange',
loading: true,
autoClose: false,
disallowClose: true,
withCloseButton: false,
icon: <IconAlertCircle />,
});
axios

View File

@@ -57,7 +57,7 @@ const useStyles = createStyles((theme) => ({
maxWidth: 540,
margin: 'auto',
marginTop: theme.spacing.xl,
marginBottom: theme.spacing.xl * 1.5,
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
},
}));

View File

@@ -2,7 +2,6 @@ import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import Consola from 'consola';
import { NotificationsProvider } from '@mantine/notifications';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { getCookie } from 'cookies-next';
@@ -11,6 +10,7 @@ import { appWithTranslation } from 'next-i18next';
import { AppProps } from 'next/app';
import Head from 'next/head';
import { useEffect, useState } from 'react';
import { Notifications } from '@mantine/notifications';
import 'video.js/dist/video-js.css';
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
@@ -112,21 +112,20 @@ function App(
withNormalizeCSS
>
<ConfigProvider>
<NotificationsProvider limit={4} position="bottom-left">
<ModalsProvider
modals={{
editApp: EditAppModal,
selectElement: SelectElementModal,
integrationOptions: WidgetsEditModal,
integrationRemove: WidgetsRemoveModal,
categoryEditModal: CategoryEditModal,
changeAppPositionModal: ChangeAppPositionModal,
changeIntegrationPositionModal: ChangeWidgetPositionModal,
}}
>
<Component {...pageProps} />
</ModalsProvider>
</NotificationsProvider>
<Notifications limit={4} position="bottom-left" />
<ModalsProvider
modals={{
editApp: EditAppModal,
selectElement: SelectElementModal,
integrationOptions: WidgetsEditModal,
integrationRemove: WidgetsRemoveModal,
categoryEditModal: CategoryEditModal,
changeAppPositionModal: ChangeAppPositionModal,
changeIntegrationPositionModal: ChangeWidgetPositionModal,
}}
>
<Component {...pageProps} />
</ModalsProvider>
</ConfigProvider>
</MantineProvider>
</ColorTheme.Provider>
@@ -150,7 +149,7 @@ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
Consola.debug(`Overriding the default color scheme with ${process.env.DEFAULT_COLOR_SCHEME}`);
}
const colorScheme: ColorScheme = process.env.DEFAULT_COLOR_SCHEME as ColorScheme ?? 'light';
const colorScheme: ColorScheme = (process.env.DEFAULT_COLOR_SCHEME as ColorScheme) ?? 'light';
return {
colorScheme: getCookie('color-scheme', ctx) || 'light',

View File

@@ -2,26 +2,31 @@ import Consola from 'consola';
import { NextApiRequest, NextApiResponse } from 'next';
function Post(req: NextApiRequest, res: NextApiResponse) {
const { tried } = req.body;
// Try to match the password with the PASSWORD env variable
if (tried === process.env.PASSWORD) {
const { tried, type = 'password' } = req.body;
// If the type of password is "edit", we run this branch to check the edit password
if (type === 'edit') {
if (tried === process.env.EDIT_MODE_PASSWORD) {
process.env.DISABLE_EDIT_MODE = process.env.DISABLE_EDIT_MODE === 'true' ? 'false' : 'true';
return res.status(200).json({
success: true,
});
}
} else if (tried === process.env.PASSWORD) {
return res.status(200).json({
success: true,
});
}
// Warn that there was a wrong password attempt (date : wrong password, person's IP)
Consola.warn(
`${new Date().toLocaleString()} : Wrong password attempt, from ${
req.headers['x-forwarded-for']
}`
);
return res.status(200).json({
return res.status(401).json({
success: false,
});
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the request is a POST or a GET
if (req.method === 'POST') {
return Post(req, res);
}

View File

@@ -110,8 +110,9 @@ const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | un
supportsMediaControl: session.SupportsMediaControl ?? false,
currentlyPlaying: session.NowPlayingItem
? {
name: session.NowPlayingItem.Name as string,
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
seasonName: session.NowPlayingItem.SeasonName as string,
episodeName: session.NowPlayingItem.Name as string,
albumName: session.NowPlayingItem.Album as string,
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
metadata: {

View File

@@ -53,6 +53,7 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) =>
title: item.title ? decode(item.title) : undefined,
content: decode(item.content),
enclosure: createEnclosure(item),
link: createLink(item),
}))
.sort((a: { pubDate: number }, b: { pubDate: number }) => {
if (!a.pubDate || !b.pubDate) {
@@ -70,6 +71,14 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) =>
});
};
const createLink = (item: any) => {
if (item.link) {
return item.link;
}
return item.guid;
};
const createEnclosure = (item: any) => {
if (item.enclosure) {
return item.enclosure;

View File

@@ -62,7 +62,7 @@ export default function AuthenticationTitle() {
title: t('notifications.checking.title'),
message: t('notifications.checking.message'),
autoClose: false,
disallowClose: true,
withCloseButton: false,
});
axios
.post('/api/configs/tryPassword', {

View File

@@ -56,7 +56,7 @@ const useStyles = createStyles((theme) => ({
fontWeight: 900,
fontSize: 110,
lineHeight: 1,
marginBottom: theme.spacing.xl * 1.5,
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
[theme.fn.smallerThan('sm')]: {
fontSize: 60,
@@ -90,7 +90,7 @@ const useStyles = createStyles((theme) => ({
maxWidth: 700,
margin: 'auto',
marginTop: theme.spacing.xl,
marginBottom: theme.spacing.xl * 1.5,
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
},
}));

View File

@@ -10,6 +10,7 @@ export type GenericSessionInfo = {
export type GenericCurrentlyPlaying = {
name: string;
seasonName: string | undefined;
episodeName: string | undefined;
albumName: string | undefined;
episodeCount: number | undefined;
type: 'audio' | 'video' | 'tv' | 'movie' | undefined;

View File

@@ -1,5 +1,6 @@
import { Box, Indicator, IndicatorProps, Popover } from '@mantine/core';
import { Box, Indicator, IndicatorProps, Popover, useMantineTheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { isToday } from '../../tools/isToday';
import { MediaList } from './MediaList';
import { MediasType } from './type';
@@ -22,12 +23,24 @@ export const CalendarDay = ({ date, medias }: CalendarDayProps) => {
withinPortal
radius="lg"
shadow="sm"
transition="pop"
transitionProps={{
transition: 'pop',
}}
onClose={close}
opened={opened}
>
<Popover.Target>
<Box onClick={open}>
<Box
onClick={open}
sx={(theme) => ({
margin: 5,
backgroundColor: isToday(date)
? theme.colorScheme === 'dark'
? theme.colors.dark[5]
: theme.colors.gray[0]
: undefined,
})}
>
<DayIndicator color="red" position="bottom-start" medias={medias.books}>
<DayIndicator color="yellow" position="top-start" medias={medias.movies}>
<DayIndicator color="blue" position="top-end" medias={medias.tvShows}>

View File

@@ -72,39 +72,15 @@ function CalendarTile({ widget }: CalendarTileProps) {
return (
<Group grow style={{ height: '100%' }}>
<Calendar
m={0}
p={0}
month={month}
// Should be offset 5px to the left
style={{ position: 'relative', top: -15 }}
onMonthChange={setMonth}
defaultDate={new Date()}
onPreviousMonth={setMonth}
onNextMonth={setMonth}
size="xs"
locale={i18n?.resolvedLanguage ?? 'en'}
fullWidth
onChange={() => {}}
firstDayOfWeek={widget.properties.sundayStart ? 'sunday' : 'monday'}
dayStyle={(date) => ({
margin: -1,
backgroundColor: isToday(date)
? colorScheme === 'dark'
? colors.dark[5]
: colors.gray[0]
: undefined,
})}
firstDayOfWeek={widget.properties.sundayStart ? 0 : 1}
hideWeekdays
styles={{
weekdayCell: {
margin: 0,
padding: 0,
},
calendarHeader: {
position: 'relative',
margin: 0,
padding: 0,
},
}}
allowLevelChange={false}
dayClassName={(_, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
date={month}
hasNextLevel={false}
renderDay={(date) => (
<CalendarDay date={date} medias={getReleasedMediasForDate(medias, date, widget)} />
)}

View File

@@ -1,4 +1,4 @@
import { createStyles, Title, useMantineTheme } from '@mantine/core';
import { createStyles, Title, useMantineTheme, getStylesRef } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { DashDotCompactNetwork, DashDotInfo } from './DashDotCompactNetwork';
import { DashDotCompactStorage } from './DashDotCompactStorage';
@@ -77,7 +77,7 @@ const useIframeSrc = (
);
};
export const useStyles = createStyles((theme, _params, getRef) => ({
export const useStyles = createStyles((theme, _params) => ({
iframe: {
flex: '1 0 auto',
maxWidth: '100%',
@@ -87,7 +87,7 @@ export const useStyles = createStyles((theme, _params, getRef) => ({
colorScheme: 'light', // fixes white borders around iframe
},
graphTitle: {
ref: getRef('graphTitle'),
ref: getStylesRef('graphTitle'),
position: 'absolute',
right: 0,
bottom: 0,
@@ -99,7 +99,7 @@ export const useStyles = createStyles((theme, _params, getRef) => ({
},
graphContainer: {
position: 'relative',
[`&:hover .${getRef('graphTitle')}`]: {
[`&:hover .${getStylesRef('graphTitle')}`]: {
opacity: 0.5,
},
},

View File

@@ -232,7 +232,7 @@ const fetchDashDotInfo = async (configName: string | undefined) => {
export const useDashDotTileStyles = createStyles((theme) => ({
graphsContainer: {
marginRight: theme.spacing.sm * -1, // fix because margin collapses weirdly
marginRight: `calc(${theme.spacing.sm} * -1)`,
},
}));

View File

@@ -12,6 +12,7 @@ import {
import { IconAlertTriangle, IconMovie } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { AppAvatar } from '../../components/AppAvatar';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
import { useConfigContext } from '../../config/provider';
import { useGetMediaServers } from '../../hooks/widgets/media-servers/useGetMediaServers';
import { defineWidget } from '../helper';
@@ -40,8 +41,9 @@ interface MediaServerWidgetProps {
function MediaServerTile({ widget }: MediaServerWidgetProps) {
const { t } = useTranslation('modules/media-server');
const { config } = useConfigContext();
const isEditMode = useEditModeStore((x) => x.enabled);
const { data, isError } = useGetMediaServers({
const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({
enabled: config !== undefined,
});
@@ -57,16 +59,28 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
);
}
if (!data) {
<Center h="100%">
<Loader />
</Center>;
if (isInitialLoading) {
return (
<Stack
align="center"
justify="center"
style={{
height: '100%',
}}
>
<Loader />
<Stack align="center" spacing={0}>
<Text>{t('descriptor.name')}</Text>
<Text color="dimmed">Homarr is loading streams...</Text>
</Stack>
</Stack>
);
}
return (
<Stack h="100%">
<ScrollArea offsetScrollbars>
<Table highlightOnHover striped>
<Table highlightOnHover>
<thead>
<tr>
<th>{t('card.table.header.session')}</th>
@@ -97,7 +111,8 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
return (
<AppAvatar
iconUrl={app.appearance.iconUrl}
color={server.success === true ? undefined : 'red'}
// If success, the color is undefined, otherwise it's red but if isFetching is true, it's yellow
color={server.success ? (isFetching ? 'yellow' : undefined) : 'red'}
/>
);
})}

View File

@@ -30,11 +30,10 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
};
const Test = Icon();
return (
<Flex wrap="nowrap" gap="sm" align="center">
<Test size={16} />
<Stack spacing={0}>
<Stack spacing={0} w={200}>
<Text lineClamp={1}>{session.currentlyPlaying.name}</Text>
{session.currentlyPlaying.albumName ? (
@@ -43,8 +42,8 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
</Text>
) : (
session.currentlyPlaying.seasonName && (
<Text lineClamp={1} color="dimmed" size="xs">
{session.currentlyPlaying.seasonName}
<Text lineClamp={2} color="dimmed" size="xs">
{session.currentlyPlaying.seasonName} - {session.currentlyPlaying.episodeName}
</Text>
)
)}

View File

@@ -16,7 +16,6 @@ import {
Title,
UnstyledButton,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import {
IconBulldozer,
IconCalendarTime,
@@ -65,7 +64,6 @@ function RssTile({ widget }: RssTileProps) {
);
const { classes } = useStyles();
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);
const { ref, height } = useElementSize();
if (!data || isLoading) {
return (
@@ -88,7 +86,7 @@ function RssTile({ widget }: RssTileProps) {
}
return (
<Stack ref={ref} h="100%">
<Stack h="100%">
<LoadingOverlay visible={loadingOverlayVisible} />
<Flex gap="md">
{data.feed.image ? (
@@ -121,7 +119,7 @@ function RssTile({ widget }: RssTileProps) {
<Card
key={index}
withBorder
component={Link}
component={Link ?? 'div'}
href={item.link}
radius="md"
target="_blank"
@@ -183,12 +181,14 @@ function RssTile({ widget }: RssTileProps) {
{data.feed.pubDate}
</Text>
</Group>
<Group>
<IconBulldozer size={14} />
<Text color="dimmed" size="sm">
{data.feed.lastBuildDate}
</Text>
</Group>
{data.feed.lastBuildDate && (
<Group>
<IconBulldozer size={14} />
<Text color="dimmed" size="sm">
{data.feed.lastBuildDate}
</Text>
</Group>
)}
{data.feed.feedUrl && (
<Group spacing="sm">
<IconSpeakerphone size={14} />

View File

@@ -35,7 +35,7 @@ interface TorrentQueueItemProps {
export const BitTorrrentQueueItem = ({ torrent, app }: TorrentQueueItemProps) => {
const [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
const theme = useMantineTheme();
const { width } = useElementSize();
const { t } = useTranslation('modules/torrents-status');
@@ -75,17 +75,17 @@ export const BitTorrrentQueueItem = ({ torrent, app }: TorrentQueueItemProps) =>
<td>
<Text size="xs">{humanFileSize(size, false)}</Text>
</td>
{width > MIN_WIDTH_MOBILE && (
{theme.fn.largerThan('xs') && (
<td>
<Text size="xs">{downloadSpeed > 0 ? `${downloadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
</td>
)}
{width > MIN_WIDTH_MOBILE && (
{theme.fn.largerThan('xs') && (
<td>
<Text size="xs">{uploadSpeed > 0 ? `${uploadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
</td>
)}
{width > MIN_WIDTH_MOBILE && (
{theme.fn.largerThan('xs') && (
<td>
<Text size="xs">{torrent.eta <= 0 ? '∞' : calculateETA(torrent.eta)}</Text>
</td>

View File

@@ -17,6 +17,7 @@ import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useTranslation } from 'next-i18next';
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
import { AppIntegrationType } from '../../types/app';
@@ -59,7 +60,6 @@ interface TorrentTileProps {
function TorrentTile({ widget }: TorrentTileProps) {
const { t } = useTranslation('modules/torrents-status');
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
const { width } = useElementSize();
const {

View File

@@ -28,6 +28,7 @@ import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { UsenetHistoryList } from './UsenetHistoryList';
import { UsenetQueueList } from './UsenetQueueList';
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
dayjs.extend(duration);
@@ -59,7 +60,6 @@ function UseNetTile({ widget }: UseNetTileProps) {
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
[];
const { ref, width, height } = useElementSize();
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
const { data } = useGetUsenetInfo({ appId: selectedAppId! });

View File

@@ -128,7 +128,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
position="center"
mt="md"
total={totalPages}
page={page}
value={page}
onChange={setPage}
/>
)}

View File

@@ -35,7 +35,7 @@ const PAGE_SIZE = 13;
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
const theme = useMantineTheme();
const { t } = useTranslation('modules/usenet');
const progressbarBreakpoint = theme.breakpoints.xs;
const progressbarBreakpoint = parseInt(theme.breakpoints.xs, 10);
const progressBreakpoint = 400;
const sizeBreakpoint = 300;
const { ref, width } = useElementSize();
@@ -177,7 +177,7 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
size="sm"
position="center"
total={totalPages}
page={page}
value={page}
onChange={setPage}
/>
)}