🔀 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

10
.deepsource.toml Normal file
View File

@@ -0,0 +1,10 @@
version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
[[transformers]]
name = "prettier"

3
.gitignore vendored
View File

@@ -48,3 +48,6 @@ data/configs
!.yarn/releases
!.yarn/sdks
!.yarn/versions
#envfiles
.env

View File

@@ -28,17 +28,17 @@
"@ctrl/qbittorrent": "^4.1.0",
"@ctrl/shared-torrent": "^4.1.1",
"@ctrl/transmission": "^4.1.1",
"@emotion/react": "^11.10.5",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",
"@jellyfin/sdk": "^0.7.0",
"@mantine/core": "^5.9.3",
"@mantine/dates": "^5.9.3",
"@mantine/dropzone": "^5.9.3",
"@mantine/form": "^5.9.3",
"@mantine/hooks": "^5.9.3",
"@mantine/modals": "^5.9.3",
"@mantine/next": "^5.9.3",
"@mantine/notifications": "^5.9.3",
"@mantine/core": "^6.0.0",
"@mantine/dates": "^6.0.0",
"@mantine/dropzone": "^6.0.0",
"@mantine/form": "^6.0.0",
"@mantine/hooks": "^6.0.0",
"@mantine/modals": "^6.0.0",
"@mantine/next": "^6.0.0",
"@mantine/notifications": "^6.0.0",
"@nivo/core": "^0.80.0",
"@nivo/line": "^0.80.0",
"@tabler/icons": "^1.106.0",

View File

@@ -2,6 +2,10 @@
"description": "Homarr is a <strong>sleek</strong>, <strong>modern</strong> dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one convenient location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.",
"contact": "Having trouble or questions? Connect with us!",
"addToDashboard": "Add to Dashboard",
"tip": "Mod refers to your modifier key, it is Ctrl and Command/Super/Windows key",
"key": "Shortcut key",
"action": "Action",
"keybinds": "Keybinds",
"metrics": {
"configurationSchemaVersion": "Configuration schema version",
"configurationsCount": "Available configurations",

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>
<Badge color="red">WARNING</Badge>
<Text color="red" size="xs">
This is an experimental feature, where the edit mode is disabled entirely - no config
<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.
</Text>
</Stack>
authentication system, which will make this obsolete."
>
<Badge color="red">WARNING</Badge>
</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
className={classes.textTransformUnset}
color={secretIsPresent ? 'green' : 'red'}
variant="dot"
>
{secretIsPresent
? t('integration.type.defined')
: t('integration.type.undefined')}
</Badge>
) : (
<Badge className={classes.textTransformUnset} color="red" variant="dot">
{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,7 +112,7 @@ function App(
withNormalizeCSS
>
<ConfigProvider>
<NotificationsProvider limit={4} position="bottom-left">
<Notifications limit={4} position="bottom-left" />
<ModalsProvider
modals={{
editApp: EditAppModal,
@@ -126,7 +126,6 @@ function App(
>
<Component {...pageProps} />
</ModalsProvider>
</NotificationsProvider>
</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%">
if (isInitialLoading) {
return (
<Stack
align="center"
justify="center"
style={{
height: '100%',
}}
>
<Loader />
</Center>;
<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>
{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}
/>
)}

6
vercel.json Normal file
View File

@@ -0,0 +1,6 @@
{
"env": {
"EDIT_MODE_PASSWORD": "edit",
"DISABLE_EDIT_MODE": "TRUE"
}
}

1429
yarn.lock

File diff suppressed because it is too large Load Diff