mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 06:55:51 +01:00
🔀 Merge branch 'dev' into tests/add-tests
This commit is contained in:
10
.deepsource.toml
Normal file
10
.deepsource.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "javascript"
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
plugins = ["react"]
|
||||||
|
|
||||||
|
[[transformers]]
|
||||||
|
name = "prettier"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ data/configs
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
|
||||||
|
#envfiles
|
||||||
|
.env
|
||||||
18
package.json
18
package.json
@@ -28,17 +28,17 @@
|
|||||||
"@ctrl/qbittorrent": "^4.1.0",
|
"@ctrl/qbittorrent": "^4.1.0",
|
||||||
"@ctrl/shared-torrent": "^4.1.1",
|
"@ctrl/shared-torrent": "^4.1.1",
|
||||||
"@ctrl/transmission": "^4.1.1",
|
"@ctrl/transmission": "^4.1.1",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@jellyfin/sdk": "^0.7.0",
|
"@jellyfin/sdk": "^0.7.0",
|
||||||
"@mantine/core": "^5.9.3",
|
"@mantine/core": "^6.0.0",
|
||||||
"@mantine/dates": "^5.9.3",
|
"@mantine/dates": "^6.0.0",
|
||||||
"@mantine/dropzone": "^5.9.3",
|
"@mantine/dropzone": "^6.0.0",
|
||||||
"@mantine/form": "^5.9.3",
|
"@mantine/form": "^6.0.0",
|
||||||
"@mantine/hooks": "^5.9.3",
|
"@mantine/hooks": "^6.0.0",
|
||||||
"@mantine/modals": "^5.9.3",
|
"@mantine/modals": "^6.0.0",
|
||||||
"@mantine/next": "^5.9.3",
|
"@mantine/next": "^6.0.0",
|
||||||
"@mantine/notifications": "^5.9.3",
|
"@mantine/notifications": "^6.0.0",
|
||||||
"@nivo/core": "^0.80.0",
|
"@nivo/core": "^0.80.0",
|
||||||
"@nivo/line": "^0.80.0",
|
"@nivo/line": "^0.80.0",
|
||||||
"@tabler/icons": "^1.106.0",
|
"@tabler/icons": "^1.106.0",
|
||||||
|
|||||||
@@ -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.",
|
"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!",
|
"contact": "Having trouble or questions? Connect with us!",
|
||||||
"addToDashboard": "Add to Dashboard",
|
"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": {
|
"metrics": {
|
||||||
"configurationSchemaVersion": "Configuration schema version",
|
"configurationSchemaVersion": "Configuration schema version",
|
||||||
"configurationsCount": "Available configurations",
|
"configurationsCount": "Available configurations",
|
||||||
|
|||||||
@@ -57,7 +57,12 @@ export default function ConfigChanger() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
>
|
>
|
||||||
<Notification loading title={t('configSelect.loadingNew')} radius="md" disallowClose>
|
<Notification
|
||||||
|
loading
|
||||||
|
title={t('configSelect.loadingNew')}
|
||||||
|
radius="md"
|
||||||
|
withCloseButton={false}
|
||||||
|
>
|
||||||
{t('configSelect.pleaseWait')}
|
{t('configSelect.pleaseWait')}
|
||||||
</Notification>
|
</Notification>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Anchor,
|
Anchor,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
createStyles,
|
createStyles,
|
||||||
Divider,
|
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
HoverCard,
|
HoverCard,
|
||||||
|
Kbd,
|
||||||
Modal,
|
Modal,
|
||||||
Stack,
|
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconAnchor,
|
IconAnchor,
|
||||||
@@ -35,6 +36,8 @@ import { useConfigContext } from '../../../../config/provider';
|
|||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||||
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
|
import Tip from '../../../layout/Tip';
|
||||||
import { usePrimaryGradient } from '../../../layout/useGradient';
|
import { usePrimaryGradient } from '../../../layout/useGradient';
|
||||||
import Credits from '../../../Settings/Common/Credits';
|
import Credits from '../../../Settings/Common/Credits';
|
||||||
|
|
||||||
@@ -50,6 +53,23 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
const informations = useInformationTableItems(newVersionAvailable);
|
const informations = useInformationTableItems(newVersionAvailable);
|
||||||
const { t } = useTranslation(['common', 'layout/modals/about']);
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => closeModal()}
|
onClose={() => closeModal()}
|
||||||
@@ -76,7 +96,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
<Trans i18nKey="layout/modals/about:description" />
|
<Trans i18nKey="layout/modals/about:description" />
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Table mb="lg" striped highlightOnHover withBorder>
|
<Table mb="lg" highlightOnHover withBorder>
|
||||||
<tbody>
|
<tbody>
|
||||||
{informations.map((item, index) => (
|
{informations.map((item, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
@@ -100,8 +120,26 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</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">
|
<Title order={6} mb="xs" align="center">
|
||||||
{t('layout/modals/about:contact')}
|
{t('layout/modals/about:contact')}
|
||||||
</Title>
|
</Title>
|
||||||
@@ -161,9 +199,9 @@ interface ExtendedInitOptions extends InitOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||||
const colorGradiant = usePrimaryGradient();
|
|
||||||
const { attributes } = usePackageAttributesStore();
|
const { attributes } = usePackageAttributesStore();
|
||||||
const { editModeEnabled } = useEditModeInformationStore();
|
const { editModeEnabled } = useEditModeInformationStore();
|
||||||
|
const { primaryColor } = useColorTheme();
|
||||||
|
|
||||||
const { configVersion } = useConfigContext();
|
const { configVersion } = useConfigContext();
|
||||||
const { configs } = useConfigStore();
|
const { configs } = useConfigStore();
|
||||||
@@ -177,15 +215,19 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconKey size={20} />,
|
icon: <IconKey size={20} />,
|
||||||
label: 'experimental_disableEditMode',
|
label: 'experimental_disableEditMode',
|
||||||
content: (
|
content: (
|
||||||
<Stack>
|
<Tooltip
|
||||||
<Badge color="red">WARNING</Badge>
|
color="red"
|
||||||
<Text color="red" size="xs">
|
withinPortal
|
||||||
This is an experimental feature, where the edit mode is disabled entirely - no config
|
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
|
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
|
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||||
authentication system, which will make this obsolete.
|
authentication system, which will make this obsolete."
|
||||||
</Text>
|
>
|
||||||
</Stack>
|
<Badge color="red">WARNING</Badge>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -201,7 +243,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconLanguage size={20} />,
|
icon: <IconLanguage size={20} />,
|
||||||
label: 'i18n',
|
label: 'i18n',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{usedI18nNamespaces.length}
|
{usedI18nNamespaces.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -210,7 +252,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconVocabulary size={20} />,
|
icon: <IconVocabulary size={20} />,
|
||||||
label: 'locales',
|
label: 'locales',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{initOptions.locales.length}
|
{initOptions.locales.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -223,7 +265,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconSchema size={20} />,
|
icon: <IconSchema size={20} />,
|
||||||
label: 'configurationSchemaVersion',
|
label: 'configurationSchemaVersion',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{configVersion}
|
{configVersion}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -232,7 +274,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconFile size={20} />,
|
icon: <IconFile size={20} />,
|
||||||
label: 'configurationsCount',
|
label: 'configurationsCount',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{configs.length}
|
{configs.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -242,7 +284,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
label: 'version',
|
label: 'version',
|
||||||
content: (
|
content: (
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{attributes.packageVersion ?? 'Unknown'}
|
{attributes.packageVersion ?? 'Unknown'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{newVersionAvailable && (
|
{newVersionAvailable && (
|
||||||
@@ -282,7 +324,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconAnchor size={20} />,
|
icon: <IconAnchor size={20} />,
|
||||||
label: 'nodeEnvironment',
|
label: 'nodeEnvironment',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{attributes.environment}
|
{attributes.environment}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Stack,
|
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
Text,
|
Text,
|
||||||
@@ -40,7 +39,7 @@ export const GenericSecretInput = ({
|
|||||||
|
|
||||||
const Icon = setIcon;
|
const Icon = setIcon;
|
||||||
|
|
||||||
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
|
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(!secretIsPresent);
|
||||||
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,26 +50,26 @@ export const GenericSecretInput = ({
|
|||||||
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
|
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
|
||||||
<Icon size={18} />
|
<Icon size={18} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack spacing={0}>
|
<Flex justify="start" align="start" direction="column">
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
<Title className={classes.subtitle} order={6}>
|
<Title className={classes.subtitle} order={6}>
|
||||||
{t(label)}
|
{t(label)}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
{secretIsPresent ? (
|
<Badge
|
||||||
<Badge className={classes.textTransformUnset} color="green" variant="dot">
|
className={classes.textTransformUnset}
|
||||||
{t('integration.type.defined')}
|
color={secretIsPresent ? 'green' : 'red'}
|
||||||
|
variant="dot"
|
||||||
|
>
|
||||||
|
{secretIsPresent
|
||||||
|
? t('integration.type.defined')
|
||||||
|
: t('integration.type.undefined')}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
|
||||||
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
|
||||||
{t('integration.type.undefined')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{type === 'private' ? (
|
{type === 'private' ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={t('integration.type.explanationPrivate')}
|
label={t('integration.type.explanationPrivate')}
|
||||||
width={200}
|
width={400}
|
||||||
multiline
|
multiline
|
||||||
withinPortal
|
withinPortal
|
||||||
withArrow
|
withArrow
|
||||||
@@ -82,7 +81,7 @@ export const GenericSecretInput = ({
|
|||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={t('integration.type.explanationPublic')}
|
label={t('integration.type.explanationPublic')}
|
||||||
width={200}
|
width={400}
|
||||||
multiline
|
multiline
|
||||||
withinPortal
|
withinPortal
|
||||||
withArrow
|
withArrow
|
||||||
@@ -94,29 +93,20 @@ export const GenericSecretInput = ({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="xs" color="dimmed">
|
<Text size="xs" color="dimmed" w={400}>
|
||||||
{type === 'private'
|
{type === 'private'
|
||||||
? 'Private: Once saved, you cannot read out this value again'
|
? 'Private: Once saved, you cannot read out this value again'
|
||||||
: 'Public: Can be read out repeatedly'}
|
: 'Public: Can be read out repeatedly'}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Flex>
|
||||||
</Group>
|
</Group>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col xs={12} md={6}>
|
<Grid.Col xs={12} md={6}>
|
||||||
<Flex gap={10} justify="end" align="end">
|
<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 ? (
|
{displayUpdateField === true ? (
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
|
required
|
||||||
|
defaultValue={value}
|
||||||
placeholder="new secret"
|
placeholder="new secret"
|
||||||
styles={{ root: { width: 200 } }}
|
styles={{ root: { width: 200 } }}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const GenericAvailableElementType = ({
|
|||||||
: image;
|
: image;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid.Col span={3}>
|
<Grid.Col span="auto">
|
||||||
<Card style={{ height: '100%' }}>
|
<Card style={{ height: '100%' }}>
|
||||||
<Stack justify="space-between" style={{ height: '100%' }}>
|
<Stack justify="space-between" style={{ height: '100%' }}>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
|
|||||||
they don't integrate with any apps and their content never changes.
|
they don't integrate with any apps and their content never changes.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Grid>
|
<Grid grow>
|
||||||
<GenericAvailableElementType
|
<GenericAvailableElementType
|
||||||
name="Static Text"
|
name="Static Text"
|
||||||
description="Display a fixed string on your dashboard"
|
description="Display a fixed string on your dashboard"
|
||||||
|
|||||||
@@ -93,12 +93,10 @@ export const AppTile = ({ className, app }: AppTileProps) => {
|
|||||||
|
|
||||||
const useStyles = createStyles((theme, _params, getRef) => ({
|
const useStyles = createStyles((theme, _params, getRef) => ({
|
||||||
image: {
|
image: {
|
||||||
ref: getRef('image'),
|
|
||||||
maxHeight: '90%',
|
maxHeight: '90%',
|
||||||
maxWidth: '90%',
|
maxWidth: '90%',
|
||||||
},
|
},
|
||||||
appName: {
|
appName: {
|
||||||
ref: getRef('appName'),
|
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
|
|||||||
@@ -18,12 +18,11 @@ interface GridstackStoreType {
|
|||||||
|
|
||||||
export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | null => {
|
export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | null => {
|
||||||
const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth);
|
const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth);
|
||||||
const { sm, xl } = useMantineTheme().breakpoints;
|
|
||||||
if (!mainAreaWidth) return null;
|
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';
|
return 'small';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function CommonSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
||||||
<Space />
|
<Space />
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const SearchEngineSelector = ({ searchEngine }: Props) => {
|
|||||||
<Space mb="md" />
|
<Space mb="md" />
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('customEngine.label')}
|
label={t('customEngine.label')}
|
||||||
|
name={t('configurationName')}
|
||||||
description={t('tips.placeholderTip')}
|
description={t('tips.placeholderTip')}
|
||||||
placeholder={t('customEngine.placeholder')}
|
placeholder={t('customEngine.placeholder')}
|
||||||
value={searchUrl}
|
value={searchUrl}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function CustomizationSettings() {
|
|||||||
const { t } = useTranslation('settings/customization/general');
|
const { t } = useTranslation('settings/customization/general');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
|
||||||
<Stack mt="xs" mb="md" spacing="xs">
|
<Stack mt="xs" mb="md" spacing="xs">
|
||||||
<Text color="dimmed">{t('text')}</Text>
|
<Text color="dimmed">{t('text')}</Text>
|
||||||
<CustomizationSettingsAccordeon />
|
<CustomizationSettingsAccordeon />
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function SettingsDrawer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
size="xl"
|
size="lg"
|
||||||
padding="lg"
|
padding="lg"
|
||||||
position="right"
|
position="right"
|
||||||
title={<Title order={5}>{t('title')}</Title>}
|
title={<Title order={5}>{t('title')}</Title>}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const ToggleEditModeAction = () => {
|
|||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const { classes } = useCardStyles(true);
|
const { classes } = useCardStyles(true);
|
||||||
|
|
||||||
useHotkeys([['ctrl+E', toggleEditMode]]);
|
useHotkeys([['mod+E', toggleEditMode]]);
|
||||||
|
|
||||||
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
|
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function Header(props: any) {
|
|||||||
>
|
>
|
||||||
<Search />
|
<Search />
|
||||||
{!editModeEnabled && <ToggleEditModeAction />}
|
{!editModeEnabled && <ToggleEditModeAction />}
|
||||||
<DockerMenuButton />
|
{!editModeEnabled && <DockerMenuButton />}
|
||||||
<Indicator
|
<Indicator
|
||||||
size={15}
|
size={15}
|
||||||
color="blue"
|
color="blue"
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ export function Search() {
|
|||||||
shadow="md"
|
shadow="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
zIndex={100}
|
zIndex={100}
|
||||||
transition="pop-top-right"
|
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
@@ -297,7 +296,7 @@ export function Search() {
|
|||||||
setSearchEngine(item);
|
setSearchEngine(item);
|
||||||
showNotification({
|
showNotification({
|
||||||
radius: 'lg',
|
radius: 'lg',
|
||||||
disallowClose: true,
|
withCloseButton: false,
|
||||||
id: 'spotlight',
|
id: 'spotlight',
|
||||||
autoClose: 1000,
|
autoClose: 1000,
|
||||||
icon: <ActionIcon size="sm">{item.icon}</ActionIcon>,
|
icon: <ActionIcon size="sm">{item.icon}</ActionIcon>,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AboutModal } from '../../Dashboard/Modals/AboutModal/AboutModal';
|
|||||||
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
|
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
|
||||||
|
import { EditModeToggle } from './SettingsMenu/EditModeToggle';
|
||||||
|
|
||||||
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
||||||
const [drawerOpened, drawer] = useDisclosure(false);
|
const [drawerOpened, drawer] = useDisclosure(false);
|
||||||
@@ -25,6 +26,7 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
|
|||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<ColorSchemeSwitch />
|
<ColorSchemeSwitch />
|
||||||
|
<EditModeToggle />
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
{!editModeEnabled && (
|
{!editModeEnabled && (
|
||||||
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
||||||
|
|||||||
78
src/components/layout/header/SettingsMenu/EditModeToggle.tsx
Normal file
78
src/components/layout/header/SettingsMenu/EditModeToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ const ConfigContext = createContext<ConfigContextType>({
|
|||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
config: undefined,
|
config: undefined,
|
||||||
configVersion: undefined,
|
configVersion: undefined,
|
||||||
increaseVersion: () => console.error('Provider not set'),
|
increaseVersion: () => {},
|
||||||
setConfigName: () => console.error('Provider not set'),
|
setConfigName: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ConfigProvider = ({ children }: { children: ReactNode }) => {
|
export const ConfigProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
|||||||
1
src/constants/constants.ts
Normal file
1
src/constants/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const MIN_WIDTH_MOBILE = 500;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { MantineSize, useMantineTheme } from '@mantine/core';
|
import { MantineSize, useMantineTheme } from '@mantine/core';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import { MIN_WIDTH_MOBILE } from '../constants/constants';
|
||||||
|
|
||||||
export const useScreenLargerThan = (size: MantineSize | number) => {
|
export const useScreenLargerThan = (size: MantineSize | number) => {
|
||||||
const { breakpoints } = useMantineTheme();
|
const { breakpoints } = useMantineTheme();
|
||||||
const pixelCount = typeof size === 'string' ? breakpoints[size] : size;
|
const pixelCount = typeof size === 'string' ? breakpoints[size] : size;
|
||||||
return useMediaQuery(`(min-width: ${pixelCount}px)`);
|
return useMediaQuery(`(min-width: ${MIN_WIDTH_MOBILE})`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function sendDockerCommand(
|
|||||||
title: `${t(`actions.${action}.start`)} ${containerName}`,
|
title: `${t(`actions.${action}.start`)} ${containerName}`,
|
||||||
message: undefined,
|
message: undefined,
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
disallowClose: true,
|
withCloseButton: false,
|
||||||
});
|
});
|
||||||
axios
|
axios
|
||||||
.get(`/api/docker/container/${containerId}?action=${action}`)
|
.get(`/api/docker/container/${containerId}?action=${action}`)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Badge, BadgeVariant, MantineSize } from '@mantine/core';
|
import { Badge, BadgeProps, MantineSize } from '@mantine/core';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Dockerode from 'dockerode';
|
import Dockerode from 'dockerode';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export default function ContainerState(props: ContainerStateProps) {
|
|||||||
const options: {
|
const options: {
|
||||||
size: MantineSize;
|
size: MantineSize;
|
||||||
radius: MantineSize;
|
radius: MantineSize;
|
||||||
variant: BadgeVariant;
|
variant: BadgeProps['variant'];
|
||||||
} = {
|
} = {
|
||||||
size: 'md',
|
size: 'md',
|
||||||
radius: 'md',
|
radius: 'md',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function DockerMenuButton(props: any) {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dockerEnabled) {
|
if (!dockerEnabled || process.env.DISABLE_EDIT_MODE === 'true') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +66,13 @@ export default function DockerMenuButton(props: any) {
|
|||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
padding="xl"
|
padding="xl"
|
||||||
position="right"
|
position="right"
|
||||||
size="full"
|
size="100%"
|
||||||
title={<ContainerActionBar selected={selection} reload={reload} />}
|
title={<ContainerActionBar selected={selection} reload={reload} />}
|
||||||
|
transitionProps={{
|
||||||
|
transition: 'pop',
|
||||||
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
drawer: {
|
content: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { IconSearch } from '@tabler/icons';
|
|||||||
import Dockerode from 'dockerode';
|
import Dockerode from 'dockerode';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
||||||
import ContainerState from './ContainerState';
|
import ContainerState from './ContainerState';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
@@ -34,7 +35,6 @@ export default function DockerTable({
|
|||||||
containers: Dockerode.ContainerInfo[];
|
containers: Dockerode.ContainerInfo[];
|
||||||
selection: Dockerode.ContainerInfo[];
|
selection: Dockerode.ContainerInfo[];
|
||||||
}) {
|
}) {
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
|
||||||
const [usedContainers, setContainers] = useState<Dockerode.ContainerInfo[]>(containers);
|
const [usedContainers, setContainers] = useState<Dockerode.ContainerInfo[]>(containers);
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ function askForMedia(type: MediaType, id: number, name: string, seasons?: number
|
|||||||
color: 'orange',
|
color: 'orange',
|
||||||
loading: true,
|
loading: true,
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
disallowClose: true,
|
withCloseButton: false,
|
||||||
icon: <IconAlertCircle />,
|
icon: <IconAlertCircle />,
|
||||||
});
|
});
|
||||||
axios
|
axios
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
maxWidth: 540,
|
maxWidth: 540,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
marginTop: theme.spacing.xl,
|
marginTop: theme.spacing.xl,
|
||||||
marginBottom: theme.spacing.xl * 1.5,
|
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from
|
|||||||
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
|
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
|
||||||
import { ModalsProvider } from '@mantine/modals';
|
import { ModalsProvider } from '@mantine/modals';
|
||||||
import Consola from 'consola';
|
import Consola from 'consola';
|
||||||
import { NotificationsProvider } from '@mantine/notifications';
|
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { getCookie } from 'cookies-next';
|
import { getCookie } from 'cookies-next';
|
||||||
@@ -11,6 +10,7 @@ import { appWithTranslation } from 'next-i18next';
|
|||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
||||||
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
||||||
@@ -112,7 +112,7 @@ function App(
|
|||||||
withNormalizeCSS
|
withNormalizeCSS
|
||||||
>
|
>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<NotificationsProvider limit={4} position="bottom-left">
|
<Notifications limit={4} position="bottom-left" />
|
||||||
<ModalsProvider
|
<ModalsProvider
|
||||||
modals={{
|
modals={{
|
||||||
editApp: EditAppModal,
|
editApp: EditAppModal,
|
||||||
@@ -126,7 +126,6 @@ function App(
|
|||||||
>
|
>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</NotificationsProvider>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</ColorTheme.Provider>
|
</ColorTheme.Provider>
|
||||||
@@ -150,7 +149,7 @@ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
|||||||
Consola.debug(`Overriding the default color scheme with ${process.env.DEFAULT_COLOR_SCHEME}`);
|
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 {
|
return {
|
||||||
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
||||||
|
|||||||
@@ -2,26 +2,31 @@ import Consola from 'consola';
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
function Post(req: NextApiRequest, res: NextApiResponse) {
|
function Post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { tried } = req.body;
|
const { tried, type = 'password' } = req.body;
|
||||||
// Try to match the password with the PASSWORD env variable
|
// If the type of password is "edit", we run this branch to check the edit password
|
||||||
if (tried === process.env.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({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Warn that there was a wrong password attempt (date : wrong password, person's IP)
|
|
||||||
Consola.warn(
|
Consola.warn(
|
||||||
`${new Date().toLocaleString()} : Wrong password attempt, from ${
|
`${new Date().toLocaleString()} : Wrong password attempt, from ${
|
||||||
req.headers['x-forwarded-for']
|
req.headers['x-forwarded-for']
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
return res.status(200).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Filter out if the request is a POST or a GET
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
return Post(req, res);
|
return Post(req, res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | un
|
|||||||
supportsMediaControl: session.SupportsMediaControl ?? false,
|
supportsMediaControl: session.SupportsMediaControl ?? false,
|
||||||
currentlyPlaying: session.NowPlayingItem
|
currentlyPlaying: session.NowPlayingItem
|
||||||
? {
|
? {
|
||||||
name: session.NowPlayingItem.Name as string,
|
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
|
||||||
seasonName: session.NowPlayingItem.SeasonName as string,
|
seasonName: session.NowPlayingItem.SeasonName as string,
|
||||||
|
episodeName: session.NowPlayingItem.Name as string,
|
||||||
albumName: session.NowPlayingItem.Album as string,
|
albumName: session.NowPlayingItem.Album as string,
|
||||||
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
|
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) =>
|
|||||||
title: item.title ? decode(item.title) : undefined,
|
title: item.title ? decode(item.title) : undefined,
|
||||||
content: decode(item.content),
|
content: decode(item.content),
|
||||||
enclosure: createEnclosure(item),
|
enclosure: createEnclosure(item),
|
||||||
|
link: createLink(item),
|
||||||
}))
|
}))
|
||||||
.sort((a: { pubDate: number }, b: { pubDate: number }) => {
|
.sort((a: { pubDate: number }, b: { pubDate: number }) => {
|
||||||
if (!a.pubDate || !b.pubDate) {
|
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) => {
|
const createEnclosure = (item: any) => {
|
||||||
if (item.enclosure) {
|
if (item.enclosure) {
|
||||||
return item.enclosure;
|
return item.enclosure;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function AuthenticationTitle() {
|
|||||||
title: t('notifications.checking.title'),
|
title: t('notifications.checking.title'),
|
||||||
message: t('notifications.checking.message'),
|
message: t('notifications.checking.message'),
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
disallowClose: true,
|
withCloseButton: false,
|
||||||
});
|
});
|
||||||
axios
|
axios
|
||||||
.post('/api/configs/tryPassword', {
|
.post('/api/configs/tryPassword', {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: 110,
|
fontSize: 110,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
marginBottom: theme.spacing.xl * 1.5,
|
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||||
|
|
||||||
[theme.fn.smallerThan('sm')]: {
|
[theme.fn.smallerThan('sm')]: {
|
||||||
fontSize: 60,
|
fontSize: 60,
|
||||||
@@ -90,7 +90,7 @@ const useStyles = createStyles((theme) => ({
|
|||||||
maxWidth: 700,
|
maxWidth: 700,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
marginTop: theme.spacing.xl,
|
marginTop: theme.spacing.xl,
|
||||||
marginBottom: theme.spacing.xl * 1.5,
|
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type GenericSessionInfo = {
|
|||||||
export type GenericCurrentlyPlaying = {
|
export type GenericCurrentlyPlaying = {
|
||||||
name: string;
|
name: string;
|
||||||
seasonName: string | undefined;
|
seasonName: string | undefined;
|
||||||
|
episodeName: string | undefined;
|
||||||
albumName: string | undefined;
|
albumName: string | undefined;
|
||||||
episodeCount: number | undefined;
|
episodeCount: number | undefined;
|
||||||
type: 'audio' | 'video' | 'tv' | 'movie' | undefined;
|
type: 'audio' | 'video' | 'tv' | 'movie' | undefined;
|
||||||
|
|||||||
@@ -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 { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { isToday } from '../../tools/isToday';
|
||||||
import { MediaList } from './MediaList';
|
import { MediaList } from './MediaList';
|
||||||
import { MediasType } from './type';
|
import { MediasType } from './type';
|
||||||
|
|
||||||
@@ -22,12 +23,24 @@ export const CalendarDay = ({ date, medias }: CalendarDayProps) => {
|
|||||||
withinPortal
|
withinPortal
|
||||||
radius="lg"
|
radius="lg"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
transition="pop"
|
transitionProps={{
|
||||||
|
transition: 'pop',
|
||||||
|
}}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<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="red" position="bottom-start" medias={medias.books}>
|
||||||
<DayIndicator color="yellow" position="top-start" medias={medias.movies}>
|
<DayIndicator color="yellow" position="top-start" medias={medias.movies}>
|
||||||
<DayIndicator color="blue" position="top-end" medias={medias.tvShows}>
|
<DayIndicator color="blue" position="top-end" medias={medias.tvShows}>
|
||||||
|
|||||||
@@ -72,39 +72,15 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
|||||||
return (
|
return (
|
||||||
<Group grow style={{ height: '100%' }}>
|
<Group grow style={{ height: '100%' }}>
|
||||||
<Calendar
|
<Calendar
|
||||||
m={0}
|
defaultDate={new Date()}
|
||||||
p={0}
|
onPreviousMonth={setMonth}
|
||||||
month={month}
|
onNextMonth={setMonth}
|
||||||
// Should be offset 5px to the left
|
|
||||||
style={{ position: 'relative', top: -15 }}
|
|
||||||
onMonthChange={setMonth}
|
|
||||||
size="xs"
|
size="xs"
|
||||||
locale={i18n?.resolvedLanguage ?? 'en'}
|
locale={i18n?.resolvedLanguage ?? 'en'}
|
||||||
fullWidth
|
firstDayOfWeek={widget.properties.sundayStart ? 0 : 1}
|
||||||
onChange={() => {}}
|
|
||||||
firstDayOfWeek={widget.properties.sundayStart ? 'sunday' : 'monday'}
|
|
||||||
dayStyle={(date) => ({
|
|
||||||
margin: -1,
|
|
||||||
backgroundColor: isToday(date)
|
|
||||||
? colorScheme === 'dark'
|
|
||||||
? colors.dark[5]
|
|
||||||
: colors.gray[0]
|
|
||||||
: undefined,
|
|
||||||
})}
|
|
||||||
hideWeekdays
|
hideWeekdays
|
||||||
styles={{
|
date={month}
|
||||||
weekdayCell: {
|
hasNextLevel={false}
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
calendarHeader: {
|
|
||||||
position: 'relative',
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
allowLevelChange={false}
|
|
||||||
dayClassName={(_, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
|
|
||||||
renderDay={(date) => (
|
renderDay={(date) => (
|
||||||
<CalendarDay date={date} medias={getReleasedMediasForDate(medias, date, widget)} />
|
<CalendarDay date={date} medias={getReleasedMediasForDate(medias, date, widget)} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 { useTranslation } from 'next-i18next';
|
||||||
import { DashDotCompactNetwork, DashDotInfo } from './DashDotCompactNetwork';
|
import { DashDotCompactNetwork, DashDotInfo } from './DashDotCompactNetwork';
|
||||||
import { DashDotCompactStorage } from './DashDotCompactStorage';
|
import { DashDotCompactStorage } from './DashDotCompactStorage';
|
||||||
@@ -77,7 +77,7 @@ const useIframeSrc = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useStyles = createStyles((theme, _params, getRef) => ({
|
export const useStyles = createStyles((theme, _params) => ({
|
||||||
iframe: {
|
iframe: {
|
||||||
flex: '1 0 auto',
|
flex: '1 0 auto',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
@@ -87,7 +87,7 @@ export const useStyles = createStyles((theme, _params, getRef) => ({
|
|||||||
colorScheme: 'light', // fixes white borders around iframe
|
colorScheme: 'light', // fixes white borders around iframe
|
||||||
},
|
},
|
||||||
graphTitle: {
|
graphTitle: {
|
||||||
ref: getRef('graphTitle'),
|
ref: getStylesRef('graphTitle'),
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@@ -99,7 +99,7 @@ export const useStyles = createStyles((theme, _params, getRef) => ({
|
|||||||
},
|
},
|
||||||
graphContainer: {
|
graphContainer: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
[`&:hover .${getRef('graphTitle')}`]: {
|
[`&:hover .${getStylesRef('graphTitle')}`]: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ const fetchDashDotInfo = async (configName: string | undefined) => {
|
|||||||
|
|
||||||
export const useDashDotTileStyles = createStyles((theme) => ({
|
export const useDashDotTileStyles = createStyles((theme) => ({
|
||||||
graphsContainer: {
|
graphsContainer: {
|
||||||
marginRight: theme.spacing.sm * -1, // fix because margin collapses weirdly
|
marginRight: `calc(${theme.spacing.sm} * -1)`,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { IconAlertTriangle, IconMovie } from '@tabler/icons';
|
import { IconAlertTriangle, IconMovie } from '@tabler/icons';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { AppAvatar } from '../../components/AppAvatar';
|
import { AppAvatar } from '../../components/AppAvatar';
|
||||||
|
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useGetMediaServers } from '../../hooks/widgets/media-servers/useGetMediaServers';
|
import { useGetMediaServers } from '../../hooks/widgets/media-servers/useGetMediaServers';
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
@@ -40,8 +41,9 @@ interface MediaServerWidgetProps {
|
|||||||
function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||||
const { t } = useTranslation('modules/media-server');
|
const { t } = useTranslation('modules/media-server');
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
|
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||||
|
|
||||||
const { data, isError } = useGetMediaServers({
|
const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({
|
||||||
enabled: config !== undefined,
|
enabled: config !== undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,16 +59,28 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (isInitialLoading) {
|
||||||
<Center h="100%">
|
return (
|
||||||
|
<Stack
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Loader />
|
<Loader />
|
||||||
</Center>;
|
<Stack align="center" spacing={0}>
|
||||||
|
<Text>{t('descriptor.name')}</Text>
|
||||||
|
<Text color="dimmed">Homarr is loading streams...</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack h="100%">
|
<Stack h="100%">
|
||||||
<ScrollArea offsetScrollbars>
|
<ScrollArea offsetScrollbars>
|
||||||
<Table highlightOnHover striped>
|
<Table highlightOnHover>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t('card.table.header.session')}</th>
|
<th>{t('card.table.header.session')}</th>
|
||||||
@@ -97,7 +111,8 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
|||||||
return (
|
return (
|
||||||
<AppAvatar
|
<AppAvatar
|
||||||
iconUrl={app.appearance.iconUrl}
|
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'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -30,11 +30,10 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Test = Icon();
|
const Test = Icon();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex wrap="nowrap" gap="sm" align="center">
|
<Flex wrap="nowrap" gap="sm" align="center">
|
||||||
<Test size={16} />
|
<Test size={16} />
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0} w={200}>
|
||||||
<Text lineClamp={1}>{session.currentlyPlaying.name}</Text>
|
<Text lineClamp={1}>{session.currentlyPlaying.name}</Text>
|
||||||
|
|
||||||
{session.currentlyPlaying.albumName ? (
|
{session.currentlyPlaying.albumName ? (
|
||||||
@@ -43,8 +42,8 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
|
|||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
session.currentlyPlaying.seasonName && (
|
session.currentlyPlaying.seasonName && (
|
||||||
<Text lineClamp={1} color="dimmed" size="xs">
|
<Text lineClamp={2} color="dimmed" size="xs">
|
||||||
{session.currentlyPlaying.seasonName}
|
{session.currentlyPlaying.seasonName} - {session.currentlyPlaying.episodeName}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
|
||||||
import {
|
import {
|
||||||
IconBulldozer,
|
IconBulldozer,
|
||||||
IconCalendarTime,
|
IconCalendarTime,
|
||||||
@@ -65,7 +64,6 @@ function RssTile({ widget }: RssTileProps) {
|
|||||||
);
|
);
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);
|
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);
|
||||||
const { ref, height } = useElementSize();
|
|
||||||
|
|
||||||
if (!data || isLoading) {
|
if (!data || isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -88,7 +86,7 @@ function RssTile({ widget }: RssTileProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack ref={ref} h="100%">
|
<Stack h="100%">
|
||||||
<LoadingOverlay visible={loadingOverlayVisible} />
|
<LoadingOverlay visible={loadingOverlayVisible} />
|
||||||
<Flex gap="md">
|
<Flex gap="md">
|
||||||
{data.feed.image ? (
|
{data.feed.image ? (
|
||||||
@@ -121,7 +119,7 @@ function RssTile({ widget }: RssTileProps) {
|
|||||||
<Card
|
<Card
|
||||||
key={index}
|
key={index}
|
||||||
withBorder
|
withBorder
|
||||||
component={Link}
|
component={Link ?? 'div'}
|
||||||
href={item.link}
|
href={item.link}
|
||||||
radius="md"
|
radius="md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -183,12 +181,14 @@ function RssTile({ widget }: RssTileProps) {
|
|||||||
{data.feed.pubDate}
|
{data.feed.pubDate}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
{data.feed.lastBuildDate && (
|
||||||
<Group>
|
<Group>
|
||||||
<IconBulldozer size={14} />
|
<IconBulldozer size={14} />
|
||||||
<Text color="dimmed" size="sm">
|
<Text color="dimmed" size="sm">
|
||||||
{data.feed.lastBuildDate}
|
{data.feed.lastBuildDate}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
)}
|
||||||
{data.feed.feedUrl && (
|
{data.feed.feedUrl && (
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<IconSpeakerphone size={14} />
|
<IconSpeakerphone size={14} />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ interface TorrentQueueItemProps {
|
|||||||
|
|
||||||
export const BitTorrrentQueueItem = ({ torrent, app }: TorrentQueueItemProps) => {
|
export const BitTorrrentQueueItem = ({ torrent, app }: TorrentQueueItemProps) => {
|
||||||
const [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
|
const [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
const theme = useMantineTheme();
|
||||||
const { width } = useElementSize();
|
const { width } = useElementSize();
|
||||||
const { t } = useTranslation('modules/torrents-status');
|
const { t } = useTranslation('modules/torrents-status');
|
||||||
|
|
||||||
@@ -75,17 +75,17 @@ export const BitTorrrentQueueItem = ({ torrent, app }: TorrentQueueItemProps) =>
|
|||||||
<td>
|
<td>
|
||||||
<Text size="xs">{humanFileSize(size, false)}</Text>
|
<Text size="xs">{humanFileSize(size, false)}</Text>
|
||||||
</td>
|
</td>
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
{theme.fn.largerThan('xs') && (
|
||||||
<td>
|
<td>
|
||||||
<Text size="xs">{downloadSpeed > 0 ? `${downloadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
|
<Text size="xs">{downloadSpeed > 0 ? `${downloadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
{theme.fn.largerThan('xs') && (
|
||||||
<td>
|
<td>
|
||||||
<Text size="xs">{uploadSpeed > 0 ? `${uploadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
|
<Text size="xs">{uploadSpeed > 0 ? `${uploadSpeed.toFixed(1)} Mb/s` : '-'}</Text>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{width > MIN_WIDTH_MOBILE && (
|
{theme.fn.largerThan('xs') && (
|
||||||
<td>
|
<td>
|
||||||
<Text size="xs">{torrent.eta <= 0 ? '∞' : calculateETA(torrent.eta)}</Text>
|
<Text size="xs">{torrent.eta <= 0 ? '∞' : calculateETA(torrent.eta)}</Text>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import dayjs from 'dayjs';
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
||||||
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
||||||
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
import { AppIntegrationType } from '../../types/app';
|
import { AppIntegrationType } from '../../types/app';
|
||||||
@@ -59,7 +60,6 @@ interface TorrentTileProps {
|
|||||||
|
|
||||||
function TorrentTile({ widget }: TorrentTileProps) {
|
function TorrentTile({ widget }: TorrentTileProps) {
|
||||||
const { t } = useTranslation('modules/torrents-status');
|
const { t } = useTranslation('modules/torrents-status');
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
|
||||||
const { width } = useElementSize();
|
const { width } = useElementSize();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { defineWidget } from '../helper';
|
|||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { UsenetHistoryList } from './UsenetHistoryList';
|
import { UsenetHistoryList } from './UsenetHistoryList';
|
||||||
import { UsenetQueueList } from './UsenetQueueList';
|
import { UsenetQueueList } from './UsenetQueueList';
|
||||||
|
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
@@ -59,7 +60,6 @@ function UseNetTile({ widget }: UseNetTileProps) {
|
|||||||
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
|
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
|
||||||
[];
|
[];
|
||||||
const { ref, width, height } = useElementSize();
|
const { ref, width, height } = useElementSize();
|
||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
|
||||||
|
|
||||||
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
||||||
const { data } = useGetUsenetInfo({ appId: selectedAppId! });
|
const { data } = useGetUsenetInfo({ appId: selectedAppId! });
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
|
|||||||
position="center"
|
position="center"
|
||||||
mt="md"
|
mt="md"
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
page={page}
|
value={page}
|
||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const PAGE_SIZE = 13;
|
|||||||
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
|
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const { t } = useTranslation('modules/usenet');
|
const { t } = useTranslation('modules/usenet');
|
||||||
const progressbarBreakpoint = theme.breakpoints.xs;
|
const progressbarBreakpoint = parseInt(theme.breakpoints.xs, 10);
|
||||||
const progressBreakpoint = 400;
|
const progressBreakpoint = 400;
|
||||||
const sizeBreakpoint = 300;
|
const sizeBreakpoint = 300;
|
||||||
const { ref, width } = useElementSize();
|
const { ref, width } = useElementSize();
|
||||||
@@ -177,7 +177,7 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
|
|||||||
size="sm"
|
size="sm"
|
||||||
position="center"
|
position="center"
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
page={page}
|
value={page}
|
||||||
onChange={setPage}
|
onChange={setPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
6
vercel.json
Normal file
6
vercel.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"EDIT_MODE_PASSWORD": "edit",
|
||||||
|
"DISABLE_EDIT_MODE": "TRUE"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user