Merge branch 'dev' into weather-module

This commit is contained in:
Thomas Camlong
2022-05-18 22:14:01 +02:00
committed by GitHub
9 changed files with 200 additions and 84 deletions

View File

@@ -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",

View File

@@ -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 -->
@@ -190,5 +191,11 @@ The Search Bar will open any Search Query after the Query URL you've specified i
**Please read our [Contribution Guidelines](/CONTRIBUTING.md)** **Please read our [Contribution Guidelines](/CONTRIBUTING.md)**
All contributions are highly appreciated. All contributions are highly appreciated.
**[⤴️ 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)** **[⤴️ Back to Top](#-table-of-contents)**

View File

@@ -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={{

View File

@@ -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"

View File

@@ -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 <SettingsIcon />
size={12}
disabled={CURRENT_VERSION === latestVersion}
offset={-3}
position="top-end"
>
<SettingsIcon />
</Indicator>
</Tooltip> </Tooltip>
</ActionIcon> </ActionIcon>
</> </>

View File

@@ -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,42 +59,88 @@ 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 spacing={0}> <Group position="left">
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg"> <Alert
<BrandGithub size={18} /> // onClick open latest release page
</ActionIcon> 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}>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<BrandGithub size={18} />
</ActionIcon>
<Text
style={{
position: 'relative',
fontSize: '0.90rem',
color: 'gray',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
<Text <Text
style={{ style={{
position: 'relative',
fontSize: '0.90rem', fontSize: '0.90rem',
color: 'gray', textAlign: 'center',
color: '#a0aec0',
}} }}
> >
{CURRENT_VERSION} Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text> </Text>
</Group> </Group>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: '#a0aec0',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
</Group> </Group>
</FooterComponent> </FooterComponent>
); );

View File

@@ -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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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;
} }