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/sdks
|
||||
!.yarn/versions
|
||||
|
||||
#envfiles
|
||||
.env
|
||||
18
package.json
18
package.json
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Badge,
|
||||
Button,
|
||||
createStyles,
|
||||
Divider,
|
||||
Grid,
|
||||
Group,
|
||||
HoverCard,
|
||||
Kbd,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconAnchor,
|
||||
@@ -35,6 +36,8 @@ import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||
import { useColorTheme } from '../../../../tools/color';
|
||||
import Tip from '../../../layout/Tip';
|
||||
import { usePrimaryGradient } from '../../../layout/useGradient';
|
||||
import Credits from '../../../Settings/Common/Credits';
|
||||
|
||||
@@ -50,6 +53,23 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
const informations = useInformationTableItems(newVersionAvailable);
|
||||
const { t } = useTranslation(['common', 'layout/modals/about']);
|
||||
|
||||
const keybinds = [
|
||||
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' },
|
||||
{ key: 'Mod + K', shortcut: 'Focus on search bar' },
|
||||
{ key: 'Mod + B', shortcut: 'Open docker widget' },
|
||||
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' },
|
||||
];
|
||||
const rows = keybinds.map((element) => (
|
||||
<tr key={element.key}>
|
||||
<td>
|
||||
<Kbd>{element.key}</Kbd>
|
||||
</td>
|
||||
<td>
|
||||
<Text>{element.shortcut}</Text>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={() => closeModal()}
|
||||
@@ -76,7 +96,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
<Trans i18nKey="layout/modals/about:description" />
|
||||
</Text>
|
||||
|
||||
<Table mb="lg" striped highlightOnHover withBorder>
|
||||
<Table mb="lg" highlightOnHover withBorder>
|
||||
<tbody>
|
||||
{informations.map((item, index) => (
|
||||
<tr key={index}>
|
||||
@@ -100,8 +120,26 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Accordion mb={5} variant="contained" radius="md">
|
||||
<Accordion.Item value="keybinds">
|
||||
<Accordion.Control icon={<IconKey size={20} />}>
|
||||
{t('layout/modals/about:keybinds')}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Table mb={5}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('layout/modals/about:key')}</th>
|
||||
<th>{t('layout/modals/about:action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
<Tip>{t('layout/modals/about:tip')}</Tip>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
|
||||
<Divider variant="dashed" mb="md" />
|
||||
<Title order={6} mb="xs" align="center">
|
||||
{t('layout/modals/about:contact')}
|
||||
</Title>
|
||||
@@ -161,9 +199,9 @@ interface ExtendedInitOptions extends InitOptions {
|
||||
}
|
||||
|
||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||
const colorGradiant = usePrimaryGradient();
|
||||
const { attributes } = usePackageAttributesStore();
|
||||
const { editModeEnabled } = useEditModeInformationStore();
|
||||
const { primaryColor } = useColorTheme();
|
||||
|
||||
const { configVersion } = useConfigContext();
|
||||
const { configs } = useConfigStore();
|
||||
@@ -177,15 +215,19 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconKey size={20} />,
|
||||
label: 'experimental_disableEditMode',
|
||||
content: (
|
||||
<Stack>
|
||||
<Tooltip
|
||||
color="red"
|
||||
withinPortal
|
||||
width={300}
|
||||
multiline
|
||||
withArrow
|
||||
label="This is an experimental feature, where the edit mode is disabled entirely - no config
|
||||
modifications are possbile anymore. All update requests for the config will be dropped
|
||||
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||
authentication system, which will make this obsolete."
|
||||
>
|
||||
<Badge color="red">WARNING</Badge>
|
||||
<Text color="red" size="xs">
|
||||
This is an experimental feature, where the edit mode is disabled entirely - no config
|
||||
modifications are possbile anymore. All update requests for the config will be dropped
|
||||
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||
authentication system, which will make this obsolete.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -201,7 +243,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconLanguage size={20} />,
|
||||
label: 'i18n',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{usedI18nNamespaces.length}
|
||||
</Badge>
|
||||
),
|
||||
@@ -210,7 +252,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconVocabulary size={20} />,
|
||||
label: 'locales',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{initOptions.locales.length}
|
||||
</Badge>
|
||||
),
|
||||
@@ -223,7 +265,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconSchema size={20} />,
|
||||
label: 'configurationSchemaVersion',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{configVersion}
|
||||
</Badge>
|
||||
),
|
||||
@@ -232,7 +274,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconFile size={20} />,
|
||||
label: 'configurationsCount',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{configs.length}
|
||||
</Badge>
|
||||
),
|
||||
@@ -242,7 +284,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
label: 'version',
|
||||
content: (
|
||||
<Group position="right">
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{attributes.packageVersion ?? 'Unknown'}
|
||||
</Badge>
|
||||
{newVersionAvailable && (
|
||||
@@ -282,7 +324,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconAnchor size={20} />,
|
||||
label: 'nodeEnvironment',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{attributes.environment}
|
||||
</Badge>
|
||||
),
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Grid,
|
||||
Group,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Text,
|
||||
@@ -40,7 +39,7 @@ export const GenericSecretInput = ({
|
||||
|
||||
const Icon = setIcon;
|
||||
|
||||
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
|
||||
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(!secretIsPresent);
|
||||
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
||||
|
||||
return (
|
||||
@@ -51,26 +50,26 @@ export const GenericSecretInput = ({
|
||||
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
|
||||
<Icon size={18} />
|
||||
</ThemeIcon>
|
||||
<Stack spacing={0}>
|
||||
<Flex justify="start" align="start" direction="column">
|
||||
<Group spacing="xs">
|
||||
<Title className={classes.subtitle} order={6}>
|
||||
{t(label)}
|
||||
</Title>
|
||||
|
||||
<Group spacing="xs">
|
||||
{secretIsPresent ? (
|
||||
<Badge className={classes.textTransformUnset} color="green" variant="dot">
|
||||
{t('integration.type.defined')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
||||
{t('integration.type.undefined')}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
className={classes.textTransformUnset}
|
||||
color={secretIsPresent ? 'green' : 'red'}
|
||||
variant="dot"
|
||||
>
|
||||
{secretIsPresent
|
||||
? t('integration.type.defined')
|
||||
: t('integration.type.undefined')}
|
||||
</Badge>
|
||||
{type === 'private' ? (
|
||||
<Tooltip
|
||||
label={t('integration.type.explanationPrivate')}
|
||||
width={200}
|
||||
width={400}
|
||||
multiline
|
||||
withinPortal
|
||||
withArrow
|
||||
@@ -82,7 +81,7 @@ export const GenericSecretInput = ({
|
||||
) : (
|
||||
<Tooltip
|
||||
label={t('integration.type.explanationPublic')}
|
||||
width={200}
|
||||
width={400}
|
||||
multiline
|
||||
withinPortal
|
||||
withArrow
|
||||
@@ -94,29 +93,20 @@ export const GenericSecretInput = ({
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
<Text size="xs" color="dimmed">
|
||||
<Text size="xs" color="dimmed" w={400}>
|
||||
{type === 'private'
|
||||
? 'Private: Once saved, you cannot read out this value again'
|
||||
: 'Public: Can be read out repeatedly'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col xs={12} md={6}>
|
||||
<Flex gap={10} justify="end" align="end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisplayUpdateField(false);
|
||||
onClickUpdateButton(undefined);
|
||||
}}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
px="xl"
|
||||
>
|
||||
{t('integration.secrets.clear')}
|
||||
</Button>
|
||||
{displayUpdateField === true ? (
|
||||
<PasswordInput
|
||||
required
|
||||
defaultValue={value}
|
||||
placeholder="new secret"
|
||||
styles={{ root: { width: 200 } }}
|
||||
{...props}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -19,7 +19,7 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
|
||||
they don'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"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -41,7 +41,7 @@ export function SettingsDrawer({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size="xl"
|
||||
size="lg"
|
||||
padding="lg"
|
||||
position="right"
|
||||
title={<Title order={5}>{t('title')}</Title>}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Header(props: any) {
|
||||
>
|
||||
<Search />
|
||||
{!editModeEnabled && <ToggleEditModeAction />}
|
||||
<DockerMenuButton />
|
||||
{!editModeEnabled && <DockerMenuButton />}
|
||||
<Indicator
|
||||
size={15}
|
||||
color="blue"
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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}>
|
||||
|
||||
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',
|
||||
config: undefined,
|
||||
configVersion: undefined,
|
||||
increaseVersion: () => console.error('Provider not set'),
|
||||
setConfigName: () => console.error('Provider not set'),
|
||||
increaseVersion: () => {},
|
||||
setConfigName: () => {},
|
||||
});
|
||||
|
||||
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 { 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})`);
|
||||
};
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)`,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from
|
||||
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import Consola from 'consola';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { getCookie } from 'cookies-next';
|
||||
@@ -11,6 +10,7 @@ import { appWithTranslation } from 'next-i18next';
|
||||
import { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import 'video.js/dist/video-js.css';
|
||||
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
|
||||
import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal';
|
||||
@@ -112,21 +112,20 @@ function App(
|
||||
withNormalizeCSS
|
||||
>
|
||||
<ConfigProvider>
|
||||
<NotificationsProvider limit={4} position="bottom-left">
|
||||
<ModalsProvider
|
||||
modals={{
|
||||
editApp: EditAppModal,
|
||||
selectElement: SelectElementModal,
|
||||
integrationOptions: WidgetsEditModal,
|
||||
integrationRemove: WidgetsRemoveModal,
|
||||
categoryEditModal: CategoryEditModal,
|
||||
changeAppPositionModal: ChangeAppPositionModal,
|
||||
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</ModalsProvider>
|
||||
</NotificationsProvider>
|
||||
<Notifications limit={4} position="bottom-left" />
|
||||
<ModalsProvider
|
||||
modals={{
|
||||
editApp: EditAppModal,
|
||||
selectElement: SelectElementModal,
|
||||
integrationOptions: WidgetsEditModal,
|
||||
integrationRemove: WidgetsRemoveModal,
|
||||
categoryEditModal: CategoryEditModal,
|
||||
changeAppPositionModal: ChangeAppPositionModal,
|
||||
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</ModalsProvider>
|
||||
</ConfigProvider>
|
||||
</MantineProvider>
|
||||
</ColorTheme.Provider>
|
||||
@@ -150,7 +149,7 @@ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
Consola.debug(`Overriding the default color scheme with ${process.env.DEFAULT_COLOR_SCHEME}`);
|
||||
}
|
||||
|
||||
const colorScheme: ColorScheme = process.env.DEFAULT_COLOR_SCHEME as ColorScheme ?? 'light';
|
||||
const colorScheme: ColorScheme = (process.env.DEFAULT_COLOR_SCHEME as ColorScheme) ?? 'light';
|
||||
|
||||
return {
|
||||
colorScheme: getCookie('color-scheme', ctx) || 'light',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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)`,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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)} />
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)`,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { IconAlertTriangle, IconMovie } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppAvatar } from '../../components/AppAvatar';
|
||||
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { useGetMediaServers } from '../../hooks/widgets/media-servers/useGetMediaServers';
|
||||
import { defineWidget } from '../helper';
|
||||
@@ -40,8 +41,9 @@ interface MediaServerWidgetProps {
|
||||
function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
const { t } = useTranslation('modules/media-server');
|
||||
const { config } = useConfigContext();
|
||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||
|
||||
const { data, isError } = useGetMediaServers({
|
||||
const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({
|
||||
enabled: config !== undefined,
|
||||
});
|
||||
|
||||
@@ -57,16 +59,28 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
<Center h="100%">
|
||||
<Loader />
|
||||
</Center>;
|
||||
if (isInitialLoading) {
|
||||
return (
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Loader />
|
||||
<Stack align="center" spacing={0}>
|
||||
<Text>{t('descriptor.name')}</Text>
|
||||
<Text color="dimmed">Homarr is loading streams...</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack h="100%">
|
||||
<ScrollArea offsetScrollbars>
|
||||
<Table highlightOnHover striped>
|
||||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('card.table.header.session')}</th>
|
||||
@@ -97,7 +111,8 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
return (
|
||||
<AppAvatar
|
||||
iconUrl={app.appearance.iconUrl}
|
||||
color={server.success === true ? undefined : 'red'}
|
||||
// If success, the color is undefined, otherwise it's red but if isFetching is true, it's yellow
|
||||
color={server.success ? (isFetching ? 'yellow' : undefined) : 'red'}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
Title,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import {
|
||||
IconBulldozer,
|
||||
IconCalendarTime,
|
||||
@@ -65,7 +64,6 @@ function RssTile({ widget }: RssTileProps) {
|
||||
);
|
||||
const { classes } = useStyles();
|
||||
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);
|
||||
const { ref, height } = useElementSize();
|
||||
|
||||
if (!data || isLoading) {
|
||||
return (
|
||||
@@ -88,7 +86,7 @@ function RssTile({ widget }: RssTileProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack ref={ref} h="100%">
|
||||
<Stack h="100%">
|
||||
<LoadingOverlay visible={loadingOverlayVisible} />
|
||||
<Flex gap="md">
|
||||
{data.feed.image ? (
|
||||
@@ -121,7 +119,7 @@ function RssTile({ widget }: RssTileProps) {
|
||||
<Card
|
||||
key={index}
|
||||
withBorder
|
||||
component={Link}
|
||||
component={Link ?? 'div'}
|
||||
href={item.link}
|
||||
radius="md"
|
||||
target="_blank"
|
||||
@@ -183,12 +181,14 @@ function RssTile({ widget }: RssTileProps) {
|
||||
{data.feed.pubDate}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<IconBulldozer size={14} />
|
||||
<Text color="dimmed" size="sm">
|
||||
{data.feed.lastBuildDate}
|
||||
</Text>
|
||||
</Group>
|
||||
{data.feed.lastBuildDate && (
|
||||
<Group>
|
||||
<IconBulldozer size={14} />
|
||||
<Text color="dimmed" size="sm">
|
||||
{data.feed.lastBuildDate}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{data.feed.feedUrl && (
|
||||
<Group spacing="sm">
|
||||
<IconSpeakerphone size={14} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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! });
|
||||
|
||||
@@ -128,7 +128,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ a
|
||||
position="center"
|
||||
mt="md"
|
||||
total={totalPages}
|
||||
page={page}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
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