Merge branch 'dev' into docker-integration

This commit is contained in:
Thomas Camlong
2022-07-20 15:05:16 +02:00
committed by GitHub
18 changed files with 463 additions and 162 deletions

View File

@@ -1,29 +1,30 @@
import { import {
Modal, ActionIcon,
Anchor,
Button,
Center, Center,
Group, Group,
TextInput,
Image, Image,
Button,
Select,
LoadingOverlay, LoadingOverlay,
ActionIcon, Modal,
Tooltip,
Title,
Anchor,
Text,
Tabs,
MultiSelect, MultiSelect,
ScrollArea, ScrollArea,
Select,
Switch, Switch,
Tabs,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { IconApps as Apps } from '@tabler/icons';
import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { IconApps as Apps } from '@tabler/icons';
import { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ServiceTypeList, StatusCodes } from '../../tools/types'; import { ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
export function AddItemShelfButton(props: any) { export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
@@ -58,7 +59,8 @@ function MatchIcon(name: string, form: any) {
fetch( fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name `https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-') .replace(/\s+/g, '-')
.toLowerCase()}.png` .toLowerCase()
.replace(/^dash\.$/, 'dashdot')}.png`
).then((res) => { ).then((res) => {
if (res.ok) { if (res.ok) {
form.setFieldValue('icon', res.url); form.setFieldValue('icon', res.url);
@@ -81,9 +83,10 @@ function MatchPort(name: string, form: any) {
{ name: 'sonarr', value: '8989' }, { name: 'sonarr', value: '8989' },
{ name: 'radarr', value: '7878' }, { name: 'radarr', value: '7878' },
{ name: 'lidarr', value: '8686' }, { name: 'lidarr', value: '8686' },
{ name: 'readarr', value: '8686' }, { name: 'readarr', value: '8787' },
{ name: 'deluge', value: '8112' }, { name: 'deluge', value: '8112' },
{ name: 'transmission', value: '9091' }, { name: 'transmission', value: '9091' },
{ name: 'dash.', value: '3001' },
]; ];
// Match name with portmap key // Match name with portmap key
const port = portmap.find((p) => p.name === name.toLowerCase()); const port = portmap.find((p) => p.name === name.toLowerCase());
@@ -275,15 +278,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
}} }}
error={form.errors.apiKey && 'Invalid API key'} error={form.errors.apiKey && 'Invalid API key'}
/> />
<Text <Tip>
style={{ Get your API key{' '}
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: 'gray',
}}
>
Tip: Get your API key{' '}
<Anchor <Anchor
target="_blank" target="_blank"
weight="bold" weight="bold"
@@ -292,7 +288,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
> >
here. here.
</Anchor> </Anchor>
</Text> </Tip>
</> </>
)} )}
{form.values.type === 'qBittorrent' && ( {form.values.type === 'qBittorrent' && (

View File

@@ -152,6 +152,7 @@ const AppShelf = (props: any) => {
const noCategory = config.services.filter( const noCategory = config.services.filter(
(e) => e.category === undefined || e.category === null (e) => e.category === undefined || e.category === null
); );
const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false;
// Create an item with 0: true, 1: true, 2: true... For each category // Create an item with 0: true, 1: true, 2: true... For each category
return ( return (
// Return one item for each category // Return one item for each category
@@ -176,6 +177,7 @@ const AppShelf = (props: any) => {
{item()} {item()}
</Accordion.Item> </Accordion.Item>
) : null} ) : null}
{downloadEnabled ? (
<Accordion.Item key="Downloads" label="Your downloads"> <Accordion.Item key="Downloads" label="Your downloads">
<Paper <Paper
p="lg" p="lg"
@@ -191,6 +193,7 @@ const AppShelf = (props: any) => {
<DownloadComponent /> <DownloadComponent />
</Paper> </Paper>
</Accordion.Item> </Accordion.Item>
) : null}
</Accordion> </Accordion>
</Group> </Group>
); );

View File

@@ -37,7 +37,7 @@ export default function TitleChanger() {
}; };
return ( return (
<Group direction="column" grow> <Group direction="column" grow mb="lg">
<form onSubmit={form.onSubmit((values) => saveChanges(values))}> <form onSubmit={form.onSubmit((values) => saveChanges(values))}>
<Group grow direction="column"> <Group grow direction="column">
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} /> <TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />

View File

@@ -1,13 +1,12 @@
import { ActionIcon, Group, Text, SegmentedControl, TextInput, Anchor } from '@mantine/core'; import { Group, Text, SegmentedControl, TextInput } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { IconBrandGithub as BrandGithub, IconBrandDiscord as BrandDiscord } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch'; import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch'; import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
import ConfigChanger from '../Config/ConfigChanger'; import ConfigChanger from '../Config/ConfigChanger';
import SaveConfigComponent from '../Config/SaveConfig'; import SaveConfigComponent from '../Config/SaveConfig';
import ModuleEnabler from './ModuleEnabler'; import ModuleEnabler from './ModuleEnabler';
import Tip from '../layout/Tip';
export default function CommonSettings(args: any) { export default function CommonSettings(args: any) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
@@ -25,20 +24,16 @@ export default function CommonSettings(args: any) {
); );
return ( return (
<Group direction="column" grow> <Group direction="column" grow mb="lg">
<Group grow direction="column" spacing={0}> <Group grow direction="column" spacing={0}>
<Text>Search engine</Text> <Text>Search engine</Text>
<Text <Tip>
style={{ Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube or
fontSize: '0.75rem', for a Torrent respectively.
color: 'gray', </Tip>
marginBottom: '0.5rem',
}}
>
Tip: %s can be used as a placeholder for the query.
</Text>
<SegmentedControl <SegmentedControl
fullWidth fullWidth
mb="sm"
title="Search engine" title="Search engine"
value={ value={
// Match config.settings.searchUrl with a key in the matches array // Match config.settings.searchUrl with a key in the matches array
@@ -60,6 +55,8 @@ export default function CommonSettings(args: any) {
data={matches} data={matches}
/> />
{searchUrl === 'Custom' && ( {searchUrl === 'Custom' && (
<>
<Tip>%s can be used as a placeholder for the query.</Tip>
<TextInput <TextInput
label="Query URL" label="Query URL"
placeholder="Custom query URL" placeholder="Custom query URL"
@@ -75,6 +72,7 @@ export default function CommonSettings(args: any) {
}); });
}} }}
/> />
</>
)} )}
</Group> </Group>
<ColorSchemeSwitch /> <ColorSchemeSwitch />
@@ -82,52 +80,7 @@ export default function CommonSettings(args: any) {
<ModuleEnabler /> <ModuleEnabler />
<ConfigChanger /> <ConfigChanger />
<SaveConfigComponent /> <SaveConfigComponent />
<Text <Tip>Upload your config file by dragging and dropping it onto the page!</Tip>
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: 'gray',
}}
>
Tip: You can upload your config file by dragging and dropping it onto the page!
</Text>
<Group position="center" direction="row" mr="xs">
<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>
<Group spacing={1}>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: 'gray',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
<BrandDiscord size={18} />
</ActionIcon>
</Group>
</Group>
</Group> </Group>
); );
} }

View File

@@ -0,0 +1,44 @@
import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants';
export default function Credits(props: any) {
return (
<Group position="center" direction="row" mr="xs">
<Group spacing={0}>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<IconBrandGithub size={18} />
</ActionIcon>
<Text
style={{
position: 'relative',
fontSize: '0.90rem',
color: 'gray',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
<Group spacing={1}>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: 'gray',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
<IconBrandDiscord size={18} />
</ActionIcon>
</Group>
</Group>
);
}

View File

@@ -1,18 +1,23 @@
import { ActionIcon, Title, Tooltip, Drawer, Tabs } from '@mantine/core'; import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks'; import { useHotkeys } from '@mantine/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { IconSettings } from '@tabler/icons'; import { IconSettings } from '@tabler/icons';
import AdvancedSettings from './AdvancedSettings'; import AdvancedSettings from './AdvancedSettings';
import CommonSettings from './CommonSettings'; import CommonSettings from './CommonSettings';
import Credits from './Credits';
function SettingsMenu(props: any) { function SettingsMenu(props: any) {
return ( return (
<Tabs grow> <Tabs grow>
<Tabs.Tab data-autofocus label="Common"> <Tabs.Tab data-autofocus label="Common">
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<CommonSettings /> <CommonSettings />
</ScrollArea>
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab label="Customizations"> <Tabs.Tab label="Customizations">
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
<AdvancedSettings /> <AdvancedSettings />
</ScrollArea>
</Tabs.Tab> </Tabs.Tab>
</Tabs> </Tabs>
); );
@@ -26,13 +31,14 @@ export function SettingsMenuButton(props: any) {
<> <>
<Drawer <Drawer
size="xl" size="xl"
padding="xl" padding="lg"
position="right" position="right"
title={<Title order={3}>Settings</Title>} title={<Title order={5}>Settings</Title>}
opened={props.opened || opened} opened={props.opened || opened}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
> >
<SettingsMenu /> <SettingsMenu />
<Credits />
</Drawer> </Drawer>
<ActionIcon <ActionIcon
variant="default" variant="default"

View File

@@ -1,24 +1,25 @@
import React from 'react';
import { import {
createStyles, ActionIcon,
Header as Head,
Group,
Box, Box,
Burger, Burger,
createStyles,
Drawer, Drawer,
Title, Group,
Header as Head,
ScrollArea, ScrollArea,
ActionIcon, Title,
Transition, Transition,
} from '@mantine/core'; } from '@mantine/core';
import { useBooleanToggle } from '@mantine/hooks'; import { useBooleanToggle } from '@mantine/hooks';
import { Logo } from './Logo';
import SearchBar from '../modules/search/SearchModule';
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem'; import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
import { SettingsMenuButton } from '../Settings/SettingsMenu'; import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
import { DashdotModule } from '../modules/dash.';
import { ModuleWrapper } from '../modules/moduleWrapper'; import { ModuleWrapper } from '../modules/moduleWrapper';
import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules'; import { CalendarModule, TotalDownloadsModule, WeatherModule, DateModule } from '../modules';
import DockerDrawer from '../Docker/DockerDrawer'; import DockerDrawer from '../Docker/DockerDrawer';
import SearchBar from '../modules/search/SearchModule';
import { SettingsMenuButton } from '../Settings/SettingsMenu';
import { Logo } from './Logo';
const HEADER_HEIGHT = 60; const HEADER_HEIGHT = 60;
@@ -86,6 +87,7 @@ export function Header(props: any) {
<ModuleWrapper module={TotalDownloadsModule} /> <ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} /> <ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} /> <ModuleWrapper module={DateModule} />
<ModuleWrapper module={DashdotModule} />
</Group> </Group>
</ScrollArea> </ScrollArea>
</div> </div>

View File

@@ -0,0 +1,19 @@
import { Text } from '@mantine/core';
interface TipProps {
children: React.ReactNode;
}
export default function Tip(props: TipProps) {
return (
<Text
style={{
fontSize: '0.75rem',
color: 'gray',
marginBottom: '0.5rem',
}}
>
Tip: {props.children}
</Text>
);
}

View File

@@ -1,6 +1,7 @@
import { Group } from '@mantine/core'; import { Group } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules'; import { CalendarModule, DateModule, TotalDownloadsModule, WeatherModule } from '../modules';
import { DashdotModule } from '../modules/dash.';
import { ModuleWrapper } from '../modules/moduleWrapper'; import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Widgets(props: any) { export default function Widgets(props: any) {
@@ -14,6 +15,7 @@ export default function Widgets(props: any) {
<ModuleWrapper module={TotalDownloadsModule} /> <ModuleWrapper module={TotalDownloadsModule} />
<ModuleWrapper module={WeatherModule} /> <ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={DateModule} /> <ModuleWrapper module={DateModule} />
<ModuleWrapper module={DashdotModule} />
</Group> </Group>
)} )}
</> </>

View File

@@ -0,0 +1,233 @@
import { createStyles, useMantineColorScheme, useMantineTheme } from '@mantine/core';
import { IconCalendar as CalendarIcon } from '@tabler/icons';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useConfig } from '../../../tools/state';
import { serviceItem } from '../../../tools/types';
import { IModule } from '../modules';
const asModule = <T extends IModule>(t: T) => t;
export const DashdotModule = asModule({
title: 'Dash.',
description: 'A module for displaying the graphs of your running Dash. instance.',
icon: CalendarIcon,
component: DashdotComponent,
options: {
cpuMultiView: {
name: 'CPU Multi-Core View',
value: false,
},
storageMultiView: {
name: 'Storage Multi-Drive View',
value: false,
},
useCompactView: {
name: 'Use Compact View',
value: false,
},
graphs: {
name: 'Graphs',
value: ['CPU', 'RAM', 'Storage', 'Network'],
options: ['CPU', 'RAM', 'Storage', 'Network', 'GPU'],
},
},
});
const useStyles = createStyles((theme, _params) => ({
heading: {
marginTop: 0,
marginBottom: 10,
},
table: {
display: 'table',
},
tableRow: {
display: 'table-row',
},
tableLabel: {
display: 'table-cell',
paddingRight: 10,
},
tableValue: {
display: 'table-cell',
whiteSpace: 'pre-wrap',
paddingBottom: 5,
},
graphsContainer: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
rowGap: 10,
columnGap: 10,
},
iframe: {
flex: '1 0 auto',
maxWidth: '100%',
height: '140px',
borderRadius: theme.radius.lg,
},
}));
const bpsPrettyPrint = (bits?: number) =>
!bits
? '-'
: bits > 1000 * 1000 * 1000
? `${(bits / 1000 / 1000 / 1000).toFixed(1)} Gb/s`
: bits > 1000 * 1000
? `${(bits / 1000 / 1000).toFixed(1)} Mb/s`
: bits > 1000
? `${(bits / 1000).toFixed(1)} Kb/s`
: `${bits.toFixed(1)} b/s`;
const bytePrettyPrint = (byte: number): string =>
byte > 1024 * 1024 * 1024
? `${(byte / 1024 / 1024 / 1024).toFixed(1)} GiB`
: byte > 1024 * 1024
? `${(byte / 1024 / 1024).toFixed(1)} MiB`
: byte > 1024
? `${(byte / 1024).toFixed(1)} KiB`
: `${byte.toFixed(1)} B`;
const useJson = (service: serviceItem | undefined, url: string) => {
const [data, setData] = useState<any | undefined>();
const doRequest = async () => {
try {
const resp = await axios.get(url, { baseURL: service?.url });
setData(resp.data);
// eslint-disable-next-line no-empty
} catch (e) {}
};
useEffect(() => {
if (service?.url) {
doRequest();
}
}, [service?.url]);
return data;
};
export function DashdotComponent() {
const { config } = useConfig();
const theme = useMantineTheme();
const { classes } = useStyles();
const { colorScheme } = useMantineColorScheme();
const dashConfig = config.modules?.[DashdotModule.title]
.options as typeof DashdotModule['options'];
const isCompact = dashConfig?.useCompactView?.value ?? false;
const dashdotService = config.services.filter((service) => service.type === 'Dash.')[0];
const enabledGraphs = dashConfig?.graphs?.value ?? ['CPU', 'RAM', 'Storage', 'Network'];
const cpuEnabled = enabledGraphs.includes('CPU');
const storageEnabled = enabledGraphs.includes('Storage');
const ramEnabled = enabledGraphs.includes('RAM');
const networkEnabled = enabledGraphs.includes('Network');
const gpuEnabled = enabledGraphs.includes('GPU');
const info = useJson(dashdotService, '/info');
const storageLoad = useJson(dashdotService, '/load/storage');
const totalUsed =
(storageLoad?.layout as any[])?.reduce((acc, curr) => (curr.load ?? 0) + acc, 0) ?? 0;
const totalSize =
(info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0;
const graphs = [
{
name: 'CPU',
enabled: cpuEnabled,
params: {
multiView: dashConfig?.cpuMultiView?.value ?? false,
},
},
{
name: 'Storage',
enabled: storageEnabled && !isCompact,
params: {
multiView: dashConfig?.storageMultiView?.value ?? false,
},
},
{
name: 'RAM',
enabled: ramEnabled,
},
{
name: 'Network',
enabled: networkEnabled,
spanTwo: true,
},
{
name: 'GPU',
enabled: gpuEnabled,
spanTwo: true,
},
].filter((g) => g.enabled);
return (
<div>
<h2 className={classes.heading}>Dash.</h2>
{!dashdotService ? (
<p>No dash. service found. Please add one to your Homarr dashboard.</p>
) : !info ? (
<p>Cannot acquire information from dash. - are you running the latest version?</p>
) : (
<div className={classes.graphsContainer}>
<div className={classes.table}>
{storageEnabled && isCompact && (
<div className={classes.tableRow}>
<p className={classes.tableLabel}>Storage:</p>
<p className={classes.tableValue}>
{(totalUsed / (totalSize || 1)).toFixed(1)}%{'\n'}
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
</p>
</div>
)}
{networkEnabled && (
<div className={classes.tableRow}>
<p className={classes.tableLabel}>Network:</p>
<p className={classes.tableValue}>
{bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'}
{bpsPrettyPrint(info?.network?.speedDown)} Down
</p>
</div>
)}
</div>
{graphs.map((graph) => (
<iframe
className={classes.iframe}
style={
isCompact
? {
width: graph.spanTwo ? '100%' : 'calc(50% - 5px)',
}
: undefined
}
key={graph.name}
title={graph.name}
src={`${
dashdotService.url
}?singleGraphMode=true&graph=${graph.name.toLowerCase()}&theme=${colorScheme}&surface=${(colorScheme ===
'dark'
? theme.colors.dark[7]
: theme.colors.gray[0]
).substring(1)}${isCompact ? '&gap=10' : '&gap=5'}&innerRadius=${theme.radius.lg}${
graph.params
? `&${Object.entries(graph.params)
.map(([key, value]) => `${key}=${value.toString()}`)
.join('&')}`
: ''
}`}
frameBorder="0"
allowTransparency
/>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1 @@
export { DashdotModule } from './DashdotModule';

View File

@@ -23,7 +23,7 @@ export default function DateComponent(props: any) {
const [date, setDate] = useState(new Date()); const [date, setDate] = useState(new Date());
const setSafeInterval = useSetSafeInterval(); const setSafeInterval = useSetSafeInterval();
const { config } = useConfig(); const { config } = useConfig();
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? false; const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? true;
const formatString = isFullTime ? 'HH:mm' : 'h:mm A'; const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
// 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 :)

View File

@@ -1,6 +1,7 @@
export * from './date';
export * from './calendar'; export * from './calendar';
export * from './search'; export * from './dash.';
export * from './ping'; export * from './date';
export * from './weather';
export * from './downloads'; export * from './downloads';
export * from './ping';
export * from './search';
export * from './weather';

View File

@@ -1,10 +1,18 @@
import { Button, Card, Group, Menu, Switch, TextInput, useMantineColorScheme } from '@mantine/core'; import {
Button,
Card,
Group,
Menu,
MultiSelect,
Switch,
TextInput,
useMantineColorScheme,
} from '@mantine/core';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { IModule } from './modules'; import { IModule } from './modules';
function getItems(module: IModule) { function getItems(module: IModule) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const enabledModules = config.modules ?? {};
const items: JSX.Element[] = []; const items: JSX.Element[] = [];
if (module.options) { if (module.options) {
const keys = Object.keys(module.options); const keys = Object.keys(module.options);
@@ -15,6 +23,38 @@ function getItems(module: IModule) {
types.forEach((type, index) => { types.forEach((type, index) => {
const optionName = `${module.title}.${keys[index]}`; const optionName = `${module.title}.${keys[index]}`;
const moduleInConfig = config.modules?.[module.title]; const moduleInConfig = config.modules?.[module.title];
if (type === 'object') {
items.push(
<MultiSelect
label={module.options?.[keys[index]].name}
data={module.options?.[keys[index]].options ?? []}
defaultValue={
(moduleInConfig?.options?.[keys[index]]?.value as string[]) ??
(values[index].value as string[]) ??
[]
}
searchable
onChange={(value) => {
setConfig({
...config,
modules: {
...config.modules,
[module.title]: {
...moduleInConfig,
options: {
...moduleInConfig?.options,
[keys[index]]: {
...moduleInConfig?.options?.[keys[index]],
value,
},
},
},
},
});
}}
/>
);
}
if (type === 'string') { if (type === 'string') {
items.push( items.push(
<form <form
@@ -44,7 +84,11 @@ function getItems(module: IModule) {
id={optionName} id={optionName}
name={optionName} name={optionName}
label={values[index].name} label={values[index].name}
defaultValue={(moduleInConfig?.options?.[keys[index]]?.value as string) ?? ''} defaultValue={
(moduleInConfig?.options?.[keys[index]]?.value as string) ??
(values[index].value as string) ??
''
}
onChange={(e) => {}} onChange={(e) => {}}
/> />
@@ -59,7 +103,9 @@ function getItems(module: IModule) {
<Switch <Switch
defaultChecked={ defaultChecked={
// Set default checked to the value of the option if it exists // Set default checked to the value of the option if it exists
(moduleInConfig?.options?.[keys[index]]?.value as boolean) ?? false (moduleInConfig?.options?.[keys[index]]?.value as boolean) ??
(values[index].value as boolean) ??
false
} }
key={keys[index]} key={keys[index]}
onClick={(e) => { onClick={(e) => {

View File

@@ -16,5 +16,6 @@ interface Option {
export interface OptionValues { export interface OptionValues {
name: string; name: string;
value: boolean | string; value: boolean | string | string[];
options?: string[];
} }

View File

@@ -1,4 +1,4 @@
import { Kbd, createStyles, Text, Popover, Autocomplete } from '@mantine/core'; import { Kbd, createStyles, Text, Popover, Autocomplete, Tooltip } from '@mantine/core';
import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks'; import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { import {
@@ -107,18 +107,6 @@ export default function SearchBar(props: any) {
}, 20); }, 20);
})} })}
> >
<Popover
opened={opened}
position="bottom"
placement="start"
width={260}
withArrow
radius="md"
trapFocus={false}
transition="pop-bottom-right"
onFocusCapture={() => setOpened(true)}
onBlurCapture={() => setOpened(false)}
target={
<Autocomplete <Autocomplete
autoFocus autoFocus
variant="filled" variant="filled"
@@ -134,13 +122,6 @@ export default function SearchBar(props: any) {
{...props} {...props}
{...form.getInputProps('query')} {...form.getInputProps('query')}
/> />
}
>
<Text>
Tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
or for a Torrent respectively.
</Text>
</Popover>
</form> </form>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Group, Space, Title, Tooltip } from '@mantine/core'; import { Group, Space, Title, Tooltip, Skeleton } from '@mantine/core';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
@@ -29,7 +29,7 @@ export const WeatherModule: IModule = {
}, },
location: { location: {
name: 'Current location', name: 'Current location',
value: '', value: 'Paris',
}, },
}, },
}; };
@@ -135,7 +135,7 @@ export default function WeatherComponent(props: any) {
const { config } = useConfig(); const { config } = useConfig();
const [weather, setWeather] = useState({} as WeatherResponse); const [weather, setWeather] = useState({} as WeatherResponse);
const cityInput: string = const cityInput: string =
(config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? ''; (config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? 'Paris';
const isFahrenheit: boolean = const isFahrenheit: boolean =
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false; (config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
@@ -157,7 +157,18 @@ export default function WeatherComponent(props: any) {
}); });
}, [cityInput]); }, [cityInput]);
if (!weather.current_weather) { if (!weather.current_weather) {
return null; return (
<>
<Skeleton height={40} width={100} mb="xl" />
<Group noWrap direction="row">
<Skeleton height={50} circle />
<Group>
<Skeleton height={25} width={70} mr="lg" />
<Skeleton height={25} width={70} />
</Group>
</Group>
</>
);
} }
function usePerferedUnit(value: number): string { function usePerferedUnit(value: number): string {
return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`; return isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;

View File

@@ -61,6 +61,7 @@ export const Targets = [
export const ServiceTypeList = [ export const ServiceTypeList = [
'Other', 'Other',
'Emby', 'Emby',
'Dash.',
'Deluge', 'Deluge',
'Lidarr', 'Lidarr',
'Plex', 'Plex',
@@ -73,6 +74,7 @@ export const ServiceTypeList = [
export type ServiceType = export type ServiceType =
| 'Other' | 'Other'
| 'Emby' | 'Emby'
| 'Dash.'
| 'Deluge' | 'Deluge'
| 'Lidarr' | 'Lidarr'
| 'Plex' | 'Plex'