mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 15:05:48 +01:00
Merge branch 'dev' into weather-module
This commit is contained in:
@@ -20,6 +20,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/no-children-prop': 'off',
|
||||||
"unused-imports/no-unused-imports": "warn",
|
"unused-imports/no-unused-imports": "warn",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-imports": "off",
|
"@typescript-eslint/no-unused-imports": "off",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
- [📊 Modules](#-modules)
|
- [📊 Modules](#-modules)
|
||||||
- [🔍 Search Bar](#-search-bar)
|
- [🔍 Search Bar](#-search-bar)
|
||||||
- [💖 Contributing](#-contributing)
|
- [💖 Contributing](#-contributing)
|
||||||
|
- [🍏 Request Icons](#-request-icons)
|
||||||
|
|
||||||
|
|
||||||
<!-- Getting Started -->
|
<!-- Getting Started -->
|
||||||
@@ -192,3 +193,9 @@ The Search Bar will open any Search Query after the Query URL you've specified i
|
|||||||
All contributions are highly appreciated.
|
All contributions are highly appreciated.
|
||||||
|
|
||||||
**[⤴️ Back to Top](#-table-of-contents)**
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
## 🍏 Request Icons
|
||||||
|
|
||||||
|
The icons used in Homarr are automatically requested from the [dashboard-icons](https://github.com/walkxhub/dashboard-icons) repo. You can make a icon request by creating an [issue](https://github.com/walkxhub/dashboard-icons/issues/new/choose).
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export function AppShelfItem(props: any) {
|
|||||||
<motion.div
|
<motion.div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 5,
|
top: 15,
|
||||||
right: 5,
|
right: 15,
|
||||||
alignSelf: 'flex-end',
|
alignSelf: 'flex-end',
|
||||||
}}
|
}}
|
||||||
animate={{
|
animate={{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Menu, Modal, Text } from '@mantine/core';
|
import { Menu, Modal, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Check, Edit, Trash } from 'tabler-icons-react';
|
import { Check, Edit, Trash } from 'tabler-icons-react';
|
||||||
@@ -8,12 +8,13 @@ import { AddAppShelfItemForm } from './AddAppShelfItem';
|
|||||||
export default function AppShelfMenu(props: any) {
|
export default function AppShelfMenu(props: any) {
|
||||||
const { service } = props;
|
const { service } = props;
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
|
const theme = useMantineTheme();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
size="xl"
|
size="xl"
|
||||||
radius="lg"
|
radius="md"
|
||||||
opened={props.opened || opened}
|
opened={props.opened || opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
title="Modify a service"
|
title="Modify a service"
|
||||||
@@ -28,7 +29,16 @@ export default function AppShelfMenu(props: any) {
|
|||||||
message="Save service"
|
message="Save service"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Menu position="right">
|
<Menu
|
||||||
|
position="right"
|
||||||
|
radius="md"
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Menu.Label>Settings</Menu.Label>
|
<Menu.Label>Settings</Menu.Label>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Indicator,
|
|
||||||
Alert,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useColorScheme } from '@mantine/hooks';
|
import { useColorScheme } from '@mantine/hooks';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AlertCircle, Settings as SettingsIcon } from 'tabler-icons-react';
|
import { Settings as SettingsIcon } from 'tabler-icons-react';
|
||||||
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
|
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||||
import ConfigChanger from '../Config/ConfigChanger';
|
import ConfigChanger from '../Config/ConfigChanger';
|
||||||
@@ -39,14 +36,6 @@ function SettingsMenu(props: any) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group direction="column" grow>
|
<Group direction="column" grow>
|
||||||
<Alert
|
|
||||||
icon={<AlertCircle size={16} />}
|
|
||||||
title="Update available"
|
|
||||||
radius="lg"
|
|
||||||
hidden={current === latest}
|
|
||||||
>
|
|
||||||
Version {latest} is available. Current: {current}
|
|
||||||
</Alert>
|
|
||||||
<Group grow direction="column" spacing={0}>
|
<Group grow direction="column" spacing={0}>
|
||||||
<Text>Search engine</Text>
|
<Text>Search engine</Text>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
@@ -108,20 +97,7 @@ function SettingsMenu(props: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsMenuButton(props: any) {
|
export function SettingsMenuButton(props: any) {
|
||||||
const [update, setUpdate] = useState(false);
|
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
|
|
||||||
useEffect(() => {
|
|
||||||
// Fetch Data here when component first mounted
|
|
||||||
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
|
||||||
res.json().then((data) => {
|
|
||||||
setLatestVersion(data.tag_name);
|
|
||||||
if (data.tag_name !== CURRENT_VERSION) {
|
|
||||||
setUpdate(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -131,7 +107,7 @@ export function SettingsMenuButton(props: any) {
|
|||||||
opened={props.opened || opened}
|
opened={props.opened || opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
>
|
>
|
||||||
<SettingsMenu current={CURRENT_VERSION} latest={latestVersion} />
|
<SettingsMenu />
|
||||||
</Modal>
|
</Modal>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -142,14 +118,7 @@ export function SettingsMenuButton(props: any) {
|
|||||||
onClick={() => setOpened(true)}
|
onClick={() => setOpened(true)}
|
||||||
>
|
>
|
||||||
<Tooltip label="Settings">
|
<Tooltip label="Settings">
|
||||||
<Indicator
|
|
||||||
size={12}
|
|
||||||
disabled={CURRENT_VERSION === latestVersion}
|
|
||||||
offset={-3}
|
|
||||||
position="top-end"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</Indicator>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
createStyles,
|
createStyles,
|
||||||
Anchor,
|
Anchor,
|
||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Footer as FooterComponent,
|
Footer as FooterComponent,
|
||||||
|
Alert,
|
||||||
|
useMantineTheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { BrandGithub } from 'tabler-icons-react';
|
import { AlertCircle, BrandGithub } from 'tabler-icons-react';
|
||||||
import { CURRENT_VERSION } from '../../../data/constants';
|
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
footer: {
|
footer: {
|
||||||
@@ -41,6 +43,8 @@ interface FooterCenteredProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Footer({ links }: FooterCenteredProps) {
|
export function Footer({ links }: FooterCenteredProps) {
|
||||||
|
const [update, setUpdate] = useState(false);
|
||||||
|
const theme = useMantineTheme();
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const items = links.map((link) => (
|
const items = links.map((link) => (
|
||||||
<Anchor<'a'>
|
<Anchor<'a'>
|
||||||
@@ -55,13 +59,58 @@ export function Footer({ links }: FooterCenteredProps) {
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
|
||||||
|
const [isOpen, setOpen] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch Data here when component first mounted
|
||||||
|
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
||||||
|
res.json().then((data) => {
|
||||||
|
setLatestVersion(data.tag_name);
|
||||||
|
if (data.tag_name !== CURRENT_VERSION) {
|
||||||
|
setUpdate(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FooterComponent
|
<FooterComponent
|
||||||
p={5}
|
p={5}
|
||||||
height="auto"
|
height="auto"
|
||||||
style={{ border: 'none', position: 'fixed', bottom: 0, right: 0 }}
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
clear: 'both',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '0',
|
||||||
|
left: '0',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Group position="right" mr="xs" mb="xs">
|
<Group position="apart" direction="row" style={{ alignItems: 'end' }} mr="xs" mb="xs">
|
||||||
|
<Group position="left">
|
||||||
|
<Alert
|
||||||
|
// onClick open latest release page
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
icon={<AlertCircle size={16} />}
|
||||||
|
title={`Updated version: ${latestVersion} is available. Current version: ${CURRENT_VERSION}`}
|
||||||
|
withCloseButton
|
||||||
|
radius="lg"
|
||||||
|
hidden={CURRENT_VERSION === latestVersion || !isOpen}
|
||||||
|
variant="outline"
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
||||||
|
},
|
||||||
|
|
||||||
|
closeButton: {
|
||||||
|
marginLeft: '5px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
children={undefined}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group position="right">
|
||||||
<Group spacing={0}>
|
<Group spacing={0}>
|
||||||
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
||||||
<BrandGithub size={18} />
|
<BrandGithub size={18} />
|
||||||
@@ -92,6 +141,7 @@ export function Footer({ links }: FooterCenteredProps) {
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
</Group>
|
||||||
</FooterComponent>
|
</FooterComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Group, Text, Title } from '@mantine/core';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Clock } from 'tabler-icons-react';
|
import { Clock } from 'tabler-icons-react';
|
||||||
|
import { useConfig } from '../../../tools/state';
|
||||||
import { IModule } from '../modules';
|
import { IModule } from '../modules';
|
||||||
|
|
||||||
export const DateModule: IModule = {
|
export const DateModule: IModule = {
|
||||||
@@ -9,33 +10,39 @@ export const DateModule: IModule = {
|
|||||||
description: 'Show the current time and date in a card',
|
description: 'Show the current time and date in a card',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
component: DateComponent,
|
component: DateComponent,
|
||||||
|
options: {
|
||||||
|
full: {
|
||||||
|
name: 'Display full time (24-hour)',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DateComponent(props: any) {
|
export default function DateComponent(props: any) {
|
||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
|
const { config } = useConfig();
|
||||||
const hours = date.getHours();
|
const hours = date.getHours();
|
||||||
const minutes = date.getMinutes();
|
const minutes = date.getMinutes();
|
||||||
|
const fullSetting = config.settings[`${DateModule.title}.full`];
|
||||||
// Change date on minute change
|
// Change date on minute change
|
||||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
setDate(new Date());
|
setDate(new Date());
|
||||||
}, 10000);
|
}, 1000 * 60);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const timeString = `${hours < 10 ? `0${hours}` : hours}:${
|
||||||
|
minutes < 10 ? `0${minutes}` : minutes
|
||||||
|
}`;
|
||||||
|
const halfTimeString = `${hours < 10 ? `${hours % 12}` : hours % 12}:${
|
||||||
|
minutes < 10 ? `0${minutes}` : minutes
|
||||||
|
} ${hours < 12 ? 'AM' : 'PM'}`;
|
||||||
|
const finalTimeString = fullSetting ? timeString : halfTimeString;
|
||||||
return (
|
return (
|
||||||
<Group direction="column">
|
<Group p="sm" direction="column">
|
||||||
<Title>
|
<Title>{finalTimeString}</Title>
|
||||||
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
|
<Text size="xl">{dayjs(date).format('dddd, MMMM D')}</Text>
|
||||||
</Title>
|
|
||||||
<Text size="xl">
|
|
||||||
{
|
|
||||||
// Use dayjs to format the date
|
|
||||||
// https://day.js.org/en/getting-started/installation/
|
|
||||||
dayjs(date).format('dddd, MMMM D')
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,82 @@
|
|||||||
import { Card, useMantineTheme } from '@mantine/core';
|
import { Card, Menu, Switch, useMantineTheme } from '@mantine/core';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { IModule } from './modules';
|
import { IModule } from './modules';
|
||||||
|
|
||||||
export function ModuleWrapper(props: any) {
|
export function ModuleWrapper(props: any) {
|
||||||
const { module }: { module: IModule } = props;
|
const { module }: { module: IModule } = props;
|
||||||
const { config } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const enabledModules = config.settings.enabledModules ?? [];
|
const enabledModules = config.settings.enabledModules ?? [];
|
||||||
// Remove 'Module' from enabled modules titles
|
// Remove 'Module' from enabled modules titles
|
||||||
const isShown = enabledModules.includes(module.title);
|
const isShown = enabledModules.includes(module.title);
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
const items: JSX.Element[] = [];
|
||||||
|
if (module.options) {
|
||||||
|
const keys = Object.keys(module.options);
|
||||||
|
const values = Object.values(module.options);
|
||||||
|
// Get the value and the name of the option
|
||||||
|
const types = values.map((v) => typeof v.value);
|
||||||
|
// Loop over all the types with a for each loop
|
||||||
|
types.forEach((type, index) => {
|
||||||
|
const optionName = `${module.title}.${keys[index]}`;
|
||||||
|
// TODO: Add support for other types
|
||||||
|
if (type === 'boolean') {
|
||||||
|
items.push(
|
||||||
|
<Switch
|
||||||
|
defaultChecked={
|
||||||
|
// Set default checked to the value of the option if it exists
|
||||||
|
config.settings[optionName] ??
|
||||||
|
(module.options && module.options[keys[index]].value) ??
|
||||||
|
false
|
||||||
|
}
|
||||||
|
defaultValue={config.settings[optionName] ?? false}
|
||||||
|
key={keys[index]}
|
||||||
|
onClick={(e) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
settings: {
|
||||||
|
...config.settings,
|
||||||
|
enabledModules: [...config.settings.enabledModules],
|
||||||
|
[optionName]: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
label={values[index].name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sussy baka
|
||||||
if (!isShown) {
|
if (!isShown) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Card hidden={!isShown} mx="sm" withBorder radius="lg" shadow="sm">
|
<Card hidden={!isShown} mx="sm" withBorder radius="lg" shadow="sm">
|
||||||
|
{module.options && (
|
||||||
|
<Menu
|
||||||
|
size="md"
|
||||||
|
shadow="xl"
|
||||||
|
closeOnItemClick={false}
|
||||||
|
radius="md"
|
||||||
|
position="left"
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 15,
|
||||||
|
right: 15,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Label>Settings</Menu.Label>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Menu.Item key={item.key}>{item}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
<module.component />
|
<module.component />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ export interface IModule {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
component: (args: any) => JSX.Element | null;
|
component: React.ComponentType;
|
||||||
props?: any;
|
options?: Option;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
[x: string]: OptionValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionValues {
|
||||||
|
name: string;
|
||||||
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user