🎨 Rename "services" to "apps" in entire project

This commit is contained in:
Manuel Ruwe
2022-12-18 22:27:01 +01:00
parent 1e0a90f2ac
commit 661c05bc50
69 changed files with 661 additions and 495 deletions

View File

@@ -1,20 +1,196 @@
{ {
"name": "default", "schemaVersion": "1.0",
"services": [ "configProperties": {
"name": "default"
},
"categories": [],
"wrappers": [
{ {
"name": "example", "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e",
"id": "09c45847-8afc-4c1a-9697-f03192de948a", "position": 1
"type": "Other",
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
} }
], ],
"settings": { "apps": [
"searchUrl": "https://google.com/search?q=" {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a",
"name": "Documentation",
"url": "https://homarr.dev",
"behaviour": {
"onClickUrl": "https://homarr.dev",
"isOpeningInNewTab": true
}, },
"modules": { "network": {
"Search Bar": { "enabledStatusChecker": false,
"enabled": true "okStatus": [
200
]
},
"appearance": {
"iconUrl": "/imgs/logo/logo.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "wrapper",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
}
},
"shape": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 3,
"height": 3
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
"name": "Support",
"url": "https://github.com/ajnart/homarr",
"behaviour": {
"onClickUrl": "https://github.com/ajnart/homarr",
"isOpeningInNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/github.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "wrapper",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
}
},
"shape": {
"location": {
"x": 3,
"y": 0
},
"size": {
"width": 3,
"height": 3
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
"name": "Community",
"url": "https://discord.com/invite/aCsmEV5RgA",
"behaviour": {
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
"isOpeningInNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/discord.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "wrapper",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
}
},
"shape": {
"location": {
"x": 6,
"y": 0
},
"size": {
"width": 3,
"height": 3
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
"name": "Donate",
"url": "https://homarr.dev/docs/community/donate",
"behaviour": {
"onClickUrl": "https://homarr.dev/docs/community/donate",
"isOpeningInNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "/imgs/logo/logo.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "wrapper",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
}
},
"shape": {
"location": {
"x": 9,
"y": 0
},
"size": {
"width": 3,
"height": 3
}
}
}
],
"integrations": [],
"settings": {
"common": {
"searchEngine": {
"type": "google",
"properties": {}
}
},
"customization": {
"layout": {
"enabledLeftSidebar": false,
"enabledRightSidebar": false,
"enabledDocker": false,
"enabledPing": false,
"enabledSearchbar": true
},
"pageTitle": "Homarr",
"logoImageUrl": "/imgs/logo/logo.png",
"faviconUrl": "/imgs/logo/logo.png",
"backgroundImageUrl": "",
"customCss": "",
"colors": {
"primary": "red",
"secondary": "orange",
"shade": 5
},
"appOpacity": 100
} }
} }
} }

View File

@@ -50,7 +50,7 @@ export const AboutModal = ({ opened, closeModal }: AboutModalProps) => {
> >
<Text mb="lg"> <Text mb="lg">
Homarr is a simple and modern homepage for your server that helps you access all of your Homarr is a simple and modern homepage for your server that helps you access all of your
services in one place. It integrates with the services you use to display useful information apps in one place. It integrates with the apps you use to display useful information
or control them. It&apos;s easy to install and supports many different devices. or control them. It&apos;s easy to install and supports many different devices.
</Text> </Text>

View File

@@ -23,7 +23,7 @@ import { UsenetModule, TorrentsModule } from '../../modules';
const AppShelf = (props: any) => { const AppShelf = (props: any) => {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
// Extract all the categories from the services in config // Extract all the categories from the services in config
const categoryList = config.services.reduce((acc, cur) => { const categoryList = config.apps.reduce((acc, cur) => {
if (cur.category && !acc.includes(cur.category)) { if (cur.category && !acc.includes(cur.category)) {
acc.push(cur.category); acc.push(cur.category);
} }
@@ -67,9 +67,9 @@ const AppShelf = (props: any) => {
if (active.id !== over.id) { if (active.id !== over.id) {
const newConfig = { ...config }; const newConfig = { ...config };
const activeIndex = newConfig.services.findIndex((e) => e.id === active.id); const activeIndex = newConfig.apps.findIndex((e) => e.id === active.id);
const overIndex = newConfig.services.findIndex((e) => e.id === over.id); const overIndex = newConfig.apps.findIndex((e) => e.id === over.id);
newConfig.services = arrayMove(newConfig.services, activeIndex, overIndex); newConfig.apps = arrayMove(newConfig.apps, activeIndex, overIndex);
setConfig(newConfig); setConfig(newConfig);
} }
@@ -78,14 +78,14 @@ const AppShelf = (props: any) => {
const getItems = (filter?: string) => { const getItems = (filter?: string) => {
// If filter is not set, return all the services without a category or a null category // If filter is not set, return all the services without a category or a null category
let filtered = config.services; let filtered = config.apps;
const modules = Object.values(Modules).map((module) => module); const modules = Object.values(Modules).map((module) => module);
if (!filter) { if (!filter) {
filtered = config.services.filter((e) => !e.category || e.category === null); filtered = config.apps.filter((e) => !e.category || e.category === null);
} }
if (filter) { if (filter) {
filtered = config.services.filter((e) => e.category === filter); filtered = config.apps.filter((e) => e.category === filter);
} }
return ( return (
<DndContext <DndContext
@@ -94,7 +94,7 @@ const AppShelf = (props: any) => {
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<SortableContext items={config.services}> <SortableContext items={config.apps}>
<Grid gutter="lg" grow={config.settings.grow}> <Grid gutter="lg" grow={config.settings.grow}>
{filtered.map((service) => ( {filtered.map((service) => (
<Grid.Col key={service.id} span="content"> <Grid.Col key={service.id} span="content">
@@ -112,7 +112,7 @@ const AppShelf = (props: any) => {
}} }}
> >
{activeId ? ( {activeId ? (
<AppShelfItem service={config.services.find((e) => e.id === activeId)} id={activeId} /> <AppShelfItem service={config.apps.find((e) => e.id === activeId)} id={activeId} />
) : null} ) : null}
</DragOverlay> </DragOverlay>
</DndContext> </DndContext>

View File

@@ -1,18 +0,0 @@
import { Avatar, Group, Text } from '@mantine/core';
interface smallServiceItem {
label: string;
icon?: string;
url?: string;
}
export default function SmallServiceItem(props: any) {
const { service }: { service: smallServiceItem } = props;
// TODO : Use Next/link
return (
<Group>
{service.icon && <Avatar src={service.icon} />}
<Text>{service.label}</Text>
</Group>
);
}

View File

@@ -2,18 +2,18 @@ import { SelectItem } from '@mantine/core';
import { closeModal, ContextModalProps } from '@mantine/modals'; import { closeModal, ContextModalProps } from '@mantine/modals';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store'; import { useConfigStore } from '../../../../config/store';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
import { ChangePositionModal } from './ChangePositionModal'; import { ChangePositionModal } from './ChangePositionModal';
type ChangeServicePositionModalInnerProps = { type ChangeAppPositionModalInnerProps = {
service: ServiceType; app: AppType;
}; };
export const ChangeServicePositionModal = ({ export const ChangeAppPositionModal = ({
id, id,
context, context,
innerProps, innerProps,
}: ContextModalProps<ChangeServicePositionModalInnerProps>) => { }: ContextModalProps<ChangeAppPositionModalInnerProps>) => {
const { name: configName } = useConfigContext(); const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig); const updateConfig = useConfigStore((x) => x.updateConfig);
@@ -24,9 +24,9 @@ export const ChangeServicePositionModal = ({
updateConfig(configName, (previousConfig) => ({ updateConfig(configName, (previousConfig) => ({
...previousConfig, ...previousConfig,
services: [ apps: [
...previousConfig.services.filter((x) => x.id !== innerProps.service.id), ...previousConfig.apps.filter((x) => x.id !== innerProps.app.id),
{ ...innerProps.service, shape: { location: { x, y }, size: { width, height } } }, { ...innerProps.app, shape: { location: { x, y }, size: { width, height } } },
], ],
})); }));
context.closeModal(id); context.closeModal(id);
@@ -45,10 +45,10 @@ export const ChangeServicePositionModal = ({
onCancel={handleCancel} onCancel={handleCancel}
widthData={widthData} widthData={widthData}
heightData={heightData} heightData={heightData}
initialX={innerProps.service.shape.location.x} initialX={innerProps.app.shape.location.x}
initialY={innerProps.service.shape.location.y} initialY={innerProps.app.shape.location.y}
initialWidth={innerProps.service.shape.size.width} initialWidth={innerProps.app.shape.size.width}
initialHeight={innerProps.service.shape.size.height} initialHeight={innerProps.app.shape.size.height}
/> />
); );
}; };

View File

@@ -13,32 +13,32 @@ import { useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store'; import { useConfigStore } from '../../../../config/store';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab'; import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab'; import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';
import { GeneralTab } from './Tabs/GeneralTab/GeneralTab'; import { GeneralTab } from './Tabs/GeneralTab/GeneralTab';
import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab'; import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab';
import { NetworkTab } from './Tabs/NetworkTab/NetworkTab'; import { NetworkTab } from './Tabs/NetworkTab/NetworkTab';
import { DebouncedServiceIcon } from './Tabs/Shared/DebouncedServiceIcon'; import { DebouncedAppIcon } from './Tabs/Shared/DebouncedAppIcon';
import { EditServiceModalTab } from './Tabs/type'; import { EditAppModalTab } from './Tabs/type';
const serviceUrlRegex = const appUrlRegex =
'(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})'; '(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^\\s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^\\s]{2,}|www.[a-zA-Z0-9]+.[^\\s]{2,})';
export const EditServiceModal = ({ export const EditAppModal = ({
context, context,
id, id,
innerProps, innerProps,
}: ContextModalProps<{ service: ServiceType; allowServiceNamePropagation: boolean }>) => { }: ContextModalProps<{ app: AppType; allowAppNamePropagation: boolean }>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { name: configName, config } = useConfigContext(); const { name: configName, config } = useConfigContext();
const updateConfig = useConfigStore((store) => store.updateConfig); const updateConfig = useConfigStore((store) => store.updateConfig);
const [allowServiceNamePropagation, setAllowServiceNamePropagation] = useState<boolean>( const [allowAppNamePropagation, setAllowAppNamePropagation] = useState<boolean>(
innerProps.allowServiceNamePropagation innerProps.allowAppNamePropagation
); );
const form = useForm<ServiceType>({ const form = useForm<AppType>({
initialValues: innerProps.service, initialValues: innerProps.app,
validate: { validate: {
name: (name) => (!name ? 'Name is required' : null), name: (name) => (!name ? 'Name is required' : null),
url: (url) => { url: (url) => {
@@ -46,7 +46,7 @@ export const EditServiceModal = ({
return 'Url is required'; return 'Url is required';
} }
if (!url.match(serviceUrlRegex)) { if (!url.match(appUrlRegex)) {
return 'Value is not a valid url'; return 'Value is not a valid url';
} }
@@ -67,7 +67,7 @@ export const EditServiceModal = ({
return null; return null;
} }
if (!url.match(serviceUrlRegex)) { if (!url.match(appUrlRegex)) {
return 'Uri override is not a valid uri'; return 'Uri override is not a valid uri';
} }
@@ -78,21 +78,21 @@ export const EditServiceModal = ({
validateInputOnChange: true, validateInputOnChange: true,
}); });
const onSubmit = (values: ServiceType) => { const onSubmit = (values: AppType) => {
if (!configName) { if (!configName) {
return; return;
} }
updateConfig(configName, (previousConfig) => ({ updateConfig(configName, (previousConfig) => ({
...previousConfig, ...previousConfig,
services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values], apps: [...previousConfig.apps.filter((x) => x.id !== form.values.id), form.values],
})); }));
// also close the parent modal // also close the parent modal
context.closeAll(); context.closeAll();
}; };
const [activeTab, setActiveTab] = useState<EditServiceModalTab>('general'); const [activeTab, setActiveTab] = useState<EditAppModalTab>('general');
const closeModal = () => { const closeModal = () => {
context.closeModal(id); context.closeModal(id);
@@ -125,17 +125,17 @@ export const EditServiceModal = ({
</Alert> </Alert>
))} ))}
<Stack spacing={0} align="center" my="lg"> <Stack spacing={0} align="center" my="lg">
<DebouncedServiceIcon form={form} width={120} height={120} /> <DebouncedAppIcon form={form} width={120} height={120} />
<Text align="center" weight="bold" size="lg" mt="md"> <Text align="center" weight="bold" size="lg" mt="md">
{form.values.name ?? 'New Service'} {form.values.name ?? 'New App'}
</Text> </Text>
</Stack> </Stack>
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onTabChange={(tab) => setActiveTab(tab as EditServiceModalTab)} onTabChange={(tab) => setActiveTab(tab as EditAppModalTab)}
defaultValue="general" defaultValue="general"
> >
<Tabs.List grow> <Tabs.List grow>
@@ -181,8 +181,8 @@ export const EditServiceModal = ({
<NetworkTab form={form} /> <NetworkTab form={form} />
<AppearanceTab <AppearanceTab
form={form} form={form}
disallowServiceNameProgagation={() => setAllowServiceNamePropagation(false)} disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
allowServiceNamePropagation={allowServiceNamePropagation} allowAppNamePropagation={allowAppNamePropagation}
/> />
<IntegrationTab form={form} /> <IntegrationTab form={form} />
</Tabs> </Tabs>
@@ -199,9 +199,3 @@ export const EditServiceModal = ({
</> </>
); );
}; };
const useStyles = createStyles(() => ({
serviceImage: {
objectFit: 'contain',
},
}));

View File

@@ -1,20 +1,20 @@
import { createStyles, Flex, Tabs, TextInput } from '@mantine/core'; import { createStyles, Flex, Tabs, TextInput } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
import { DebouncedServiceIcon } from '../Shared/DebouncedServiceIcon'; import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon';
import { IconSelector } from './IconSelector/IconSelector'; import { IconSelector } from './IconSelector/IconSelector';
interface AppearanceTabProps { interface AppearanceTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
disallowServiceNameProgagation: () => void; disallowAppNameProgagation: () => void;
allowServiceNamePropagation: boolean; allowAppNamePropagation: boolean;
} }
export const AppearanceTab = ({ export const AppearanceTab = ({
form, form,
disallowServiceNameProgagation, disallowAppNameProgagation,
allowServiceNamePropagation, allowAppNamePropagation,
}: AppearanceTabProps) => { }: AppearanceTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('');
const { classes } = useStyles(); const { classes } = useStyles();
@@ -25,9 +25,9 @@ export const AppearanceTab = ({
<TextInput <TextInput
defaultValue={form.values.appearance.iconUrl} defaultValue={form.values.appearance.iconUrl}
className={classes.textInput} className={classes.textInput}
icon={<DebouncedServiceIcon form={form} width={20} height={20} />} icon={<DebouncedAppIcon form={form} width={20} height={20} />}
label="Service Icon" label="App Icon"
description="Logo of your service displayed in your dashboard. Must return a body content containg an image" description="Logo of your app displayed in your dashboard. Must return a body content containg an image"
variant="default" variant="default"
withAsterisk withAsterisk
required required
@@ -40,9 +40,9 @@ export const AppearanceTab = ({
iconUrl: item.url, iconUrl: item.url,
}, },
}); });
disallowServiceNameProgagation(); disallowAppNameProgagation();
}} }}
allowServiceNamePropagation={allowServiceNamePropagation} allowAppNamePropagation={allowAppNamePropagation}
form={form} form={form}
/> />
</Flex> </Flex>

View File

@@ -21,17 +21,17 @@ import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants'
import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery'; import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery';
import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem'; import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem';
import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository'; import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository';
import { ServiceType } from '../../../../../../../types/service'; import { AppType } from '../../../../../../../types/app';
interface IconSelectorProps { interface IconSelectorProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
onChange: (icon: IconSelectorItem) => void; onChange: (icon: IconSelectorItem) => void;
allowServiceNamePropagation: boolean; allowAppNamePropagation: boolean;
} }
export const IconSelector = ({ export const IconSelector = ({
onChange, onChange,
allowServiceNamePropagation, allowAppNamePropagation,
form, form,
}: IconSelectorProps) => { }: IconSelectorProps) => {
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({ const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
@@ -48,7 +48,7 @@ export const IconSelector = ({
const [debouncedValue] = useDebouncedValue(form.values.name, 500); const [debouncedValue] = useDebouncedValue(form.values.name, 500);
useEffect(() => { useEffect(() => {
if (allowServiceNamePropagation !== true) { if (allowAppNamePropagation !== true) {
return; return;
} }

View File

@@ -2,10 +2,10 @@ import { Tabs, TextInput, Switch, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconClick } from '@tabler/icons'; import { IconClick } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
interface BehaviourTabProps { interface BehaviourTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
} }
export const BehaviourTab = ({ form }: BehaviourTabProps) => { export const BehaviourTab = ({ form }: BehaviourTabProps) => {
@@ -15,8 +15,8 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
<TextInput <TextInput
icon={<IconClick size={16} />} icon={<IconClick size={16} />}
label="On click url" label="On click url"
description="Overrides the service URL when clicking on the service" description="Overrides the app URL when clicking on the app"
placeholder="URL that should be opened instead when clicking on the service" placeholder="URL that should be opened instead when clicking on the app"
variant="default" variant="default"
mb="md" mb="md"
{...form.getInputProps('behaviour.onClickUrl')} {...form.getInputProps('behaviour.onClickUrl')}

View File

@@ -2,12 +2,12 @@ import { Tabs, Text, TextInput } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconCursorText, IconLink } from '@tabler/icons'; import { IconCursorText, IconLink } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
import { EditServiceModalTab } from '../type'; import { EditAppModalTab } from '../type';
interface GeneralTabProps { interface GeneralTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
openTab: (tab: EditServiceModalTab) => void; openTab: (tab: EditAppModalTab) => void;
} }
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
@@ -16,9 +16,9 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
<Tabs.Panel value="general" pt="lg"> <Tabs.Panel value="general" pt="lg">
<TextInput <TextInput
icon={<IconCursorText size={16} />} icon={<IconCursorText size={16} />}
label="Service name" label="App name"
description="Used for displaying the service on the dashboard" description="Used for displaying the app on the dashboard"
placeholder="My example service" placeholder="My example app"
variant="default" variant="default"
mb="md" mb="md"
withAsterisk withAsterisk
@@ -26,10 +26,10 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
/> />
<TextInput <TextInput
icon={<IconLink size={16} />} icon={<IconLink size={16} />}
label="Service url" label="App url"
description={ description={
<Text> <Text>
URL that will be opened when clicking on the service. Can be overwritten using URL that will be opened when clicking on the app. Can be overwritten using
<Text <Text
onClick={() => openTab('behaviour')} onClick={() => openTab('behaviour')}
variant="link" variant="link"

View File

@@ -7,13 +7,13 @@ import {
IntegrationField, IntegrationField,
integrationFieldDefinitions, integrationFieldDefinitions,
integrationFieldProperties, integrationFieldProperties,
ServiceIntegrationPropertyType, AppIntegrationPropertyType,
ServiceIntegrationType, AppIntegrationType,
ServiceType, AppType,
} from '../../../../../../../../types/service'; } from '../../../../../../../../types/app';
interface IntegrationSelectorProps { interface IntegrationSelectorProps {
form: UseFormReturnType<ServiceType, (item: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (item: AppType) => AppType>;
} }
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
@@ -53,9 +53,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
}, },
].filter((x) => Object.keys(integrationFieldProperties).includes(x.value)); ].filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
const getNewProperties = (value: string | null): ServiceIntegrationPropertyType[] => { const getNewProperties = (value: string | null): AppIntegrationPropertyType[] => {
if (!value) return []; if (!value) return [];
const integrationType = value as ServiceIntegrationType['type']; const integrationType = value as AppIntegrationType['type'];
if (integrationType === null) { if (integrationType === null) {
return []; return [];
} }
@@ -77,7 +77,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
return ( return (
<Select <Select
label="Integration configuration" label="Integration configuration"
description="Treats this service as the selected integration and provides you with per-service configuration" description="Treats this app as the selected integration and provides you with per-app configuration"
placeholder="Select your desired configuration" placeholder="Select your desired configuration"
itemComponent={SelectItemComponent} itemComponent={SelectItemComponent}
data={data} data={data}

View File

@@ -5,13 +5,13 @@ import {
IntegrationField, IntegrationField,
integrationFieldDefinitions, integrationFieldDefinitions,
integrationFieldProperties, integrationFieldProperties,
ServiceIntegrationPropertyType, AppIntegrationPropertyType,
ServiceType, AppType,
} from '../../../../../../../../types/service'; } from '../../../../../../../../types/app';
import { GenericSecretInput } from '../InputElements/GenericSecretInput'; import { GenericSecretInput } from '../InputElements/GenericSecretInput';
interface IntegrationOptionsRendererProps { interface IntegrationOptionsRendererProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
} }
export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => { export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererProps) => {
@@ -34,7 +34,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
const type = Object.entries(integrationFieldDefinitions).find( const type = Object.entries(integrationFieldDefinitions).find(
([k, v]) => k === property ([k, v]) => k === property
)![1].type; )![1].type;
const newProperty: ServiceIntegrationPropertyType = { const newProperty: AppIntegrationPropertyType = {
type, type,
field: property as IntegrationField, field: property as IntegrationField,
isDefined: false, isDefined: false,

View File

@@ -2,12 +2,12 @@ import { Alert, Divider, Tabs, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconAlertTriangle } from '@tabler/icons'; import { IconAlertTriangle } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector'; import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer'; import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
interface IntegrationTabProps { interface IntegrationTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
} }
export const IntegrationTab = ({ form }: IntegrationTabProps) => { export const IntegrationTab = ({ form }: IntegrationTabProps) => {

View File

@@ -2,10 +2,10 @@ import { Tabs, Switch, MultiSelect } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { StatusCodes } from '../../../../../../tools/acceptableStatusCodes'; import { StatusCodes } from '../../../../../../tools/acceptableStatusCodes';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
interface NetworkTabProps { interface NetworkTabProps {
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
} }
export const NetworkTab = ({ form }: NetworkTabProps) => { export const NetworkTab = ({ form }: NetworkTabProps) => {
@@ -14,7 +14,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
<Tabs.Panel value="network" pt="lg"> <Tabs.Panel value="network" pt="lg">
<Switch <Switch
label="Enable status checker" label="Enable status checker"
description="Sends a simple HTTP / HTTPS request to check if your service is online" description="Sends a simple HTTP / HTTPS request to check if your app is online"
mb="md" mb="md"
defaultChecked={form.values.network.enabledStatusChecker} defaultChecked={form.values.network.enabledStatusChecker}
{...form.getInputProps('network.enabledStatusChecker')} {...form.getInputProps('network.enabledStatusChecker')}
@@ -23,7 +23,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
<MultiSelect <MultiSelect
required required
label="HTTP status codes" label="HTTP status codes"
description="Determines what response codes are allowed for this service to be 'Online'" description="Determines what response codes are allowed for this app to be 'Online'"
data={StatusCodes} data={StatusCodes}
clearable clearable
searchable searchable

View File

@@ -4,21 +4,21 @@ import Image from 'next/image';
import { createStyles, Loader } from '@mantine/core'; import { createStyles, Loader } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
interface DebouncedServiceIconProps { interface DebouncedAppIconProps {
width: number; width: number;
height: number; height: number;
form: UseFormReturnType<ServiceType, (values: ServiceType) => ServiceType>; form: UseFormReturnType<AppType, (values: AppType) => AppType>;
debouncedWaitPeriod?: number; debouncedWaitPeriod?: number;
} }
export const DebouncedServiceIcon = ({ export const DebouncedAppIcon = ({
form, form,
width, width,
height, height,
debouncedWaitPeriod = 1000, debouncedWaitPeriod = 1000,
}: DebouncedServiceIconProps) => { }: DebouncedAppIconProps) => {
const { classes } = useStyles(); const { classes } = useStyles();
const [debouncedIconImageUrl] = useDebouncedValue( const [debouncedIconImageUrl] = useDebouncedValue(
form.values.appearance.iconUrl, form.values.appearance.iconUrl,

View File

@@ -1,4 +1,4 @@
export type EditServiceModalTab = export type EditAppModalTab =
| 'general' | 'general'
| 'behaviour' | 'behaviour'
| 'network' | 'network'

View File

@@ -23,7 +23,7 @@ export const AvailableIntegrationElements = ({
<SelectorBackArrow onClickBack={onClickBack} /> <SelectorBackArrow onClickBack={onClickBack} />
<Text mb="md" color="dimmed"> <Text mb="md" color="dimmed">
Integrations interact with your services, to provide you with more control over your Integrations interact with your apps, to provide you with more control over your
applications. They usually require a few configurations before use. applications. They usually require a few configurations before use.
</Text> </Text>

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { openContextModalGeneric } from '../../../../../../tools/mantineModalManagerExtensions'; import { openContextModalGeneric } from '../../../../../../tools/mantineModalManagerExtensions';
import { ServiceType } from '../../../../../../types/service'; import { AppType } from '../../../../../../types/app';
import { useStyles } from '../Shared/styles'; import { useStyles } from '../Shared/styles';
interface AvailableElementTypesProps { interface AvailableElementTypesProps {
@@ -24,16 +24,16 @@ export const AvailableElementTypes = ({
<Space h="lg" /> <Space h="lg" />
<Group spacing="md" grow> <Group spacing="md" grow>
<ElementItem <ElementItem
name="Service" name="Apps"
icon={<IconBox size={40} strokeWidth={1.3} />} icon={<IconBox size={40} strokeWidth={1.3} />}
onClick={() => { onClick={() => {
openContextModalGeneric<{ service: ServiceType; allowServiceNamePropagation: boolean }>( openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>(
{ {
modal: 'editService', modal: 'editApp',
innerProps: { innerProps: {
service: { app: {
id: uuidv4(), id: uuidv4(),
name: 'Your service', name: 'Your app',
url: 'https://homarr.dev', url: 'https://homarr.dev',
appearance: { appearance: {
iconUrl: '/imgs/logo/logo.png', iconUrl: '/imgs/logo/logo.png',
@@ -67,7 +67,7 @@ export const AvailableElementTypes = ({
properties: [], properties: [],
}, },
}, },
allowServiceNamePropagation: true, allowAppNamePropagation: true,
}, },
size: 'xl', size: 'xl',
} }

View File

@@ -16,7 +16,7 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
<Text mb="md" color="dimmed"> <Text mb="md" color="dimmed">
Static elements provide you additional control over your dashboard. They are static, because Static elements provide you additional control over your dashboard. They are static, because
they don&apos;t integrate with any services and their content never changes. they don&apos;t integrate with any apps and their content never changes.
</Text> </Text>
<Grid> <Grid>

View File

@@ -3,4 +3,4 @@ interface ServiceIconProps {
service: string; service: string;
} }
export const ServiceIcon = ({ size }: ServiceIconProps) => null; export const AppIcon = ({ size }: ServiceIconProps) => null;

View File

@@ -1,19 +1,19 @@
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions'; import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
import { GenericTileMenu } from '../GenericTileMenu'; import { GenericTileMenu } from '../GenericTileMenu';
interface TileMenuProps { interface TileMenuProps {
service: ServiceType; app: AppType;
} }
export const ServiceMenu = ({ service }: TileMenuProps) => { export const AppMenu = ({ app }: TileMenuProps) => {
const handleClickEdit = () => { const handleClickEdit = () => {
openContextModalGeneric<{ service: ServiceType; allowServiceNamePropagation: boolean }>({ openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
modal: 'editService', modal: 'editApp',
size: 'xl', size: 'xl',
innerProps: { innerProps: {
service, app,
allowServiceNamePropagation: false, allowAppNamePropagation: false,
}, },
styles: { styles: {
root: { root: {
@@ -25,9 +25,9 @@ export const ServiceMenu = ({ service }: TileMenuProps) => {
const handleClickChangePosition = () => { const handleClickChangePosition = () => {
openContextModalGeneric({ openContextModalGeneric({
modal: 'changeServicePositionModal', modal: 'changeAppPositionModal',
innerProps: { innerProps: {
service, app,
}, },
styles: { styles: {
root: { root: {

View File

@@ -3,23 +3,23 @@ import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
interface ServicePingProps { interface AppPingProps {
service: ServiceType; app: AppType;
} }
export const ServicePing = ({ service }: ServicePingProps) => { export const AppPing = ({ app }: AppPingProps) => {
const { t } = useTranslation('modules/ping'); const { t } = useTranslation('modules/ping');
const { config } = useConfigContext(); const { config } = useConfigContext();
const active = const active =
(config?.settings.customization.layout.enabledPing && service.network.enabledStatusChecker) ?? (config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ??
false; false;
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: [`ping/${service.id}`], queryKey: [`ping/${app.id}`],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`/api/modules/ping?url=${encodeURI(service.url)}`); const response = await fetch(`/api/modules/ping?url=${encodeURI(app.url)}`);
const isOk = service.network.okStatus.includes(response.status); const isOk = app.network.okStatus.includes(response.status);
return { return {
status: response.status, status: response.status,
state: isOk ? 'online' : 'down', state: isOk ? 'online' : 'down',

View File

@@ -1,19 +1,19 @@
import { Card, Center, Text, UnstyledButton } from '@mantine/core'; import { Card, Center, Text, UnstyledButton } from '@mantine/core';
import { NextLink } from '@mantine/next'; import { NextLink } from '@mantine/next';
import { createStyles } from '@mantine/styles'; import { createStyles } from '@mantine/styles';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
import { useCardStyles } from '../../../layout/useCardStyles'; import { useCardStyles } from '../../../layout/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore'; import { useEditModeStore } from '../../Views/useEditModeStore';
import { HomarrCardWrapper } from '../HomarrCardWrapper'; import { HomarrCardWrapper } from '../HomarrCardWrapper';
import { BaseTileProps } from '../type'; import { BaseTileProps } from '../type';
import { ServiceMenu } from './ServiceMenu'; import { AppMenu } from './AppMenu';
import { ServicePing } from './ServicePing'; import { AppPing } from './AppPing';
interface ServiceTileProps extends BaseTileProps { interface AppTileProps extends BaseTileProps {
service: ServiceType; app: AppType;
} }
export const ServiceTile = ({ className, service }: ServiceTileProps) => { export const AppTile = ({ className, app }: AppTileProps) => {
const isEditMode = useEditModeStore((x) => x.enabled); const isEditMode = useEditModeStore((x) => x.enabled);
const { cx, classes } = useStyles(); const { cx, classes } = useStyles();
@@ -24,25 +24,25 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
const inner = ( const inner = (
<> <>
<Text align="center" weight={500} size="md" className={classes.serviceName}> <Text align="center" weight={500} size="md" className={classes.appName}>
{service.name} {app.name}
</Text> </Text>
<Center style={{ height: '75%', flex: 1 }}> <Center style={{ height: '75%', flex: 1 }}>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img className={classes.image} src={service.appearance.iconUrl} alt="" /> <img className={classes.image} src={app.appearance.iconUrl} alt="" />
</Center> </Center>
</> </>
); );
return ( return (
<HomarrCardWrapper className={className}> <HomarrCardWrapper className={className}>
{/* TODO: add service menu */} {/* TODO: add app menu */}
<div style={{ position: 'absolute', top: 10, right: 10 }}> <div style={{ position: 'absolute', top: 10, right: 10 }}>
<ServiceMenu service={service} /> <AppMenu app={app} />
</div> </div>
{!service.url || isEditMode ? ( {!app.url || isEditMode ? (
<UnstyledButton <UnstyledButton
className={classes.button} className={classes.button}
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }} style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
@@ -53,14 +53,14 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
<UnstyledButton <UnstyledButton
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }} style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
component={NextLink} component={NextLink}
href={service.url} href={app.url}
target={service.behaviour.isOpeningNewTab ? '_blank' : '_self'} target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
className={cx(classes.button, classes.link)} className={cx(classes.button, classes.link)}
> >
{inner} {inner}
</UnstyledButton> </UnstyledButton>
)} )}
<ServicePing service={service} /> <AppPing app={app} />
</HomarrCardWrapper> </HomarrCardWrapper>
); );
}; };
@@ -72,8 +72,8 @@ const useStyles = createStyles((theme, _params, getRef) => ({
maxWidth: '80%', maxWidth: '80%',
transition: 'transform 100ms ease-in-out', transition: 'transform 100ms ease-in-out',
}, },
serviceName: { appName: {
ref: getRef('serviceName'), ref: getRef('appName'),
}, },
button: { button: {
height: '100%', height: '100%',
@@ -87,8 +87,8 @@ const useStyles = createStyles((theme, _params, getRef) => ({
[`&:hover .${getRef('image')}`]: { [`&:hover .${getRef('image')}`]: {
// TODO: add styles for image when hovering card // TODO: add styles for image when hovering card
}, },
[`&:hover .${getRef('serviceName')}`]: { [`&:hover .${getRef('appName')}`]: {
// TODO: add styles for service name when hovering card // TODO: add styles for app name when hovering card
}, },
}, },
})); }));

View File

@@ -2,7 +2,7 @@ import { ReactNode, RefObject } from 'react';
interface GridstackTileWrapperProps { interface GridstackTileWrapperProps {
id: string; id: string;
type: 'service' | 'module'; type: 'app' | 'module';
x?: number; x?: number;
y?: number; y?: number;
width?: number; width?: number;

View File

@@ -5,11 +5,11 @@ import { DashDotTile } from '../../../widgets/dashDot/DashDotTile';
import { UseNetTile } from '../../../widgets/useNet/UseNetTile'; import { UseNetTile } from '../../../widgets/useNet/UseNetTile';
import { WeatherTile } from '../../../widgets/weather/WeatherTile'; import { WeatherTile } from '../../../widgets/weather/WeatherTile';
import { EmptyTile } from './EmptyTile'; import { EmptyTile } from './EmptyTile';
import { ServiceTile } from './Service/ServiceTile'; import { AppTile } from './Apps/AppTile';
// TODO: just remove and use service (later app) directly. For widgets the the definition should contain min/max width/height // TODO: just remove and use app (later app) directly. For widgets the the definition should contain min/max width/height
type TileDefinitionProps = { type TileDefinitionProps = {
[key in keyof IntegrationsType | 'service']: { [key in keyof IntegrationsType | 'app']: {
minWidth?: number; minWidth?: number;
minHeight?: number; minHeight?: number;
maxWidth?: number; maxWidth?: number;
@@ -20,8 +20,8 @@ type TileDefinitionProps = {
// TODO: change components for other modules // TODO: change components for other modules
export const Tiles: TileDefinitionProps = { export const Tiles: TileDefinitionProps = {
service: { app: {
component: ServiceTile, component: AppTile,
minWidth: 2, minWidth: 2,
maxWidth: 12, maxWidth: 12,
minHeight: 2, minHeight: 2,

View File

@@ -27,19 +27,19 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
data-category={category.id} data-category={category.id}
ref={refs.wrapper} ref={refs.wrapper}
> >
{items?.map((service) => { {items?.map((app) => {
const { component: TileComponent, ...tile } = Tiles['service']; const { component: TileComponent, ...tile } = Tiles['app'];
return ( return (
<GridstackTileWrapper <GridstackTileWrapper
id={service.id} id={app.id}
type="service" type="app"
key={service.id} key={app.id}
itemRef={refs.items.current[service.id]} itemRef={refs.items.current[app.id]}
{...tile} {...tile}
{...service.shape.location} {...app.shape.location}
{...service.shape.size} {...app.shape.size}
> >
<TileComponent className="grid-stack-item-content" service={service} /> <TileComponent className="grid-stack-item-content" app={app} />
</GridstackTileWrapper> </GridstackTileWrapper>
); );
})} })}

View File

@@ -29,19 +29,19 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
gs-min-row={minRow} gs-min-row={minRow}
ref={refs.wrapper} ref={refs.wrapper}
> >
{items.map((service) => { {items.map((app) => {
const { component: TileComponent, ...tile } = Tiles['service']; const { component: TileComponent, ...tile } = Tiles['app'];
return ( return (
<GridstackTileWrapper <GridstackTileWrapper
id={service.id} id={app.id}
type="service" type="app"
key={service.id} key={app.id}
itemRef={refs.items.current[service.id]} itemRef={refs.items.current[app.id]}
{...tile} {...tile}
{...service.shape.location} {...app.shape.location}
{...service.shape.size} {...app.shape.size}
> >
<TileComponent className="grid-stack-item-content" service={service} /> <TileComponent className="grid-stack-item-content" app={app} />
</GridstackTileWrapper> </GridstackTileWrapper>
); );
})} })}

View File

@@ -17,19 +17,19 @@ export const DashboardWrapper = ({ wrapper }: DashboardWrapperProps) => {
data-wrapper={wrapper.id} data-wrapper={wrapper.id}
ref={refs.wrapper} ref={refs.wrapper}
> >
{items?.map((service) => { {items?.map((app) => {
const { component: TileComponent, ...tile } = Tiles['service']; const { component: TileComponent, ...tile } = Tiles['app'];
return ( return (
<GridstackTileWrapper <GridstackTileWrapper
id={service.id} id={app.id}
type="service" type="app"
key={service.id} key={app.id}
itemRef={refs.items.current[service.id]} itemRef={refs.items.current[app.id]}
{...tile} {...tile}
{...service.shape.location} {...app.shape.location}
{...service.shape.size} {...app.shape.size}
> >
<TileComponent className="grid-stack-item-content" service={service} /> <TileComponent className="grid-stack-item-content" app={app} />
</GridstackTileWrapper> </GridstackTileWrapper>
); );
})} })}

View File

@@ -1,7 +1,7 @@
import { GridStack, GridStackNode } from 'fily-publish-gridstack'; import { GridStack, GridStackNode } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject } from 'react'; import { MutableRefObject, RefObject } from 'react';
import { IntegrationsType } from '../../../../types/integration'; import { IntegrationsType } from '../../../../types/integration';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
export const initializeGridstack = ( export const initializeGridstack = (
areaType: 'wrapper' | 'category' | 'sidebar', areaType: 'wrapper' | 'category' | 'sidebar',
@@ -9,7 +9,7 @@ export const initializeGridstack = (
gridRef: MutableRefObject<GridStack | undefined>, gridRef: MutableRefObject<GridStack | undefined>,
itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>, itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>,
areaId: string, areaId: string,
items: ServiceType[], items: AppType[],
integrations: IntegrationsType, integrations: IntegrationsType,
isEditMode: boolean, isEditMode: boolean,
events: { events: {

View File

@@ -12,13 +12,13 @@ import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store'; import { useConfigStore } from '../../../../config/store';
import { useResize } from '../../../../hooks/use-resize'; import { useResize } from '../../../../hooks/use-resize';
import { IntegrationsType } from '../../../../types/integration'; import { IntegrationsType } from '../../../../types/integration';
import { ServiceType } from '../../../../types/service'; import { AppType } from '../../../../types/app';
import { TileBaseType } from '../../../../types/tile'; import { TileBaseType } from '../../../../types/tile';
import { useEditModeStore } from '../../Views/useEditModeStore'; import { useEditModeStore } from '../../Views/useEditModeStore';
import { initializeGridstack } from './init-gridstack'; import { initializeGridstack } from './init-gridstack';
interface UseGristackReturnType { interface UseGristackReturnType {
items: ServiceType[]; items: AppType[];
integrations: Partial<IntegrationsType>; integrations: Partial<IntegrationsType>;
refs: { refs: {
wrapper: RefObject<HTMLDivElement>; wrapper: RefObject<HTMLDivElement>;
@@ -45,7 +45,7 @@ export const useGridstack = (
const items = useMemo( const items = useMemo(
() => () =>
config?.services.filter( config?.apps.filter(
(x) => (x) =>
x.area.type === areaType && x.area.type === areaType &&
(x.area.type === 'sidebar' (x.area.type === 'sidebar'
@@ -100,8 +100,8 @@ export const useGridstack = (
// Updates the config and defines the new position of the item // Updates the config and defines the new position of the item
updateConfig(configName, (previous) => { updateConfig(configName, (previous) => {
const currentItem = const currentItem =
itemType === 'service' itemType === 'app'
? previous.services.find((x) => x.id === itemId) ? previous.apps.find((x) => x.id === itemId)
: previous.integrations[itemId as keyof typeof previous.integrations]; : previous.integrations[itemId as keyof typeof previous.integrations];
if (!currentItem) return previous; if (!currentItem) return previous;
@@ -116,12 +116,12 @@ export const useGridstack = (
}, },
}; };
if (itemType === 'service') { if (itemType === 'app') {
return { return {
...previous, ...previous,
services: [ apps: [
...previous.services.filter((x) => x.id !== itemId), ...previous.apps.filter((x) => x.id !== itemId),
{ ...(currentItem as ServiceType) }, { ...(currentItem as AppType) },
], ],
}; };
} }
@@ -147,8 +147,8 @@ export const useGridstack = (
// Updates the config and defines the new position and wrapper of the item // Updates the config and defines the new position and wrapper of the item
updateConfig(configName, (previous) => { updateConfig(configName, (previous) => {
const currentItem = const currentItem =
itemType === 'service' itemType === 'app'
? previous.services.find((x) => x.id === itemId) ? previous.apps.find((x) => x.id === itemId)
: previous.integrations[itemId as keyof typeof previous.integrations]; : previous.integrations[itemId as keyof typeof previous.integrations];
if (!currentItem) return previous; if (!currentItem) return previous;
@@ -180,12 +180,12 @@ export const useGridstack = (
}, },
}; };
if (itemType === 'service') { if (itemType === 'app') {
return { return {
...previous, ...previous,
services: [ apps: [
...previous.services.filter((x) => x.id !== itemId), ...previous.apps.filter((x) => x.id !== itemId),
{ ...(currentItem as ServiceType) }, { ...(currentItem as AppType) },
], ],
}; };
} }

View File

@@ -94,7 +94,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
<Text color="dimmed" size="xs" align="center"> <Text color="dimmed" size="xs" align="center">
Only for Only for
<br /> <br />
services &<br /> apps &<br />
integrations integrations
</Text> </Text>
</Flex> </Flex>
@@ -115,7 +115,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
<Text color="dimmed" size="xs" align="center"> <Text color="dimmed" size="xs" align="center">
Only for Only for
<br /> <br />
services &<br /> apps &<br />
integrations integrations
</Text> </Text>
</Flex> </Flex>
@@ -126,13 +126,13 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
<Stack spacing="xs"> <Stack spacing="xs">
<Checkbox <Checkbox
label="Enable left sidebar" label="Enable left sidebar"
description="Optional. Can be used for services and integrations only" description="Optional. Can be used for apps and integrations only"
checked={leftSidebar} checked={leftSidebar}
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)} onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
/> />
<Checkbox <Checkbox
label="Enable right sidebar" label="Enable right sidebar"
description="Optional. Can be used for services and integrations only" description="Optional. Can be used for apps and integrations only"
checked={rightSidebar} checked={rightSidebar}
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)} onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
/> />

View File

@@ -16,7 +16,7 @@ import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/i
import axios from 'axios'; import axios from 'axios';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { forwardRef, useEffect, useRef, useState } from 'react'; import React, { forwardRef, useEffect, useRef, useState } from 'react';
import SmallServiceItem from '../../AppShelf/SmallServiceItem'; import SmallAppItem from './SmallAppItem';
import Tip from '../Tip'; import Tip from '../Tip';
import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector'; import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector';
import { useConfigContext } from '../../../config/provider'; import { useConfigContext } from '../../../config/provider';
@@ -56,12 +56,12 @@ export function Search() {
const [debounced, cancel] = useDebouncedValue(searchQuery, 250); const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
// TODO: ask manuel-rw about overseerr // TODO: ask manuel-rw about overseerr
// Answer: We can simply check if there is a service of the type overseer and display results if there is one. // Answer: We can simply check if there is a app of the type overseer and display results if there is one.
// Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results // Overseerr is not use anywhere else, so it makes no sense to add a standalone toggle for displaying results
const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr; const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr;
const overseerrService = config?.services.find( const overseerrApp = config?.apps.find(
(service) => (app) =>
service.integration?.type === 'overseerr' || service.integration?.type === 'jellyseerr' app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
); );
const searchEngineSettings = config?.settings.common.searchEngine; const searchEngineSettings = config?.settings.common.searchEngine;
const searchEngineUrl = !searchEngineSettings const searchEngineUrl = !searchEngineSettings
@@ -100,32 +100,32 @@ export function Search() {
}, },
{ {
icon: <IconMovie />, icon: <IconMovie />,
disabled: !(isOverseerrEnabled === true && overseerrService !== undefined), disabled: !(isOverseerrEnabled === true && overseerrApp !== undefined),
label: t('searchEngines.overseerr.name'), label: t('searchEngines.overseerr.name'),
value: 'overseerr', value: 'overseerr',
description: t('searchEngines.overseerr.description'), description: t('searchEngines.overseerr.description'),
url: `${overseerrService?.url}search?query=`, url: `${overseerrApp?.url}search?query=`,
shortcut: 'm', shortcut: 'm',
}, },
]; ];
const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]); const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]);
const matchingServices = const matchingApps =
config?.services.filter((service) => { config?.apps.filter((app) => {
if (searchQuery === '' || searchQuery === undefined) { if (searchQuery === '' || searchQuery === undefined) {
return false; return false;
} }
return service.name.toLowerCase().includes(searchQuery.toLowerCase()); return app.name.toLowerCase().includes(searchQuery.toLowerCase());
}) ?? []; }) ?? [];
const autocompleteData = matchingServices.map((service) => ({ const autocompleteData = matchingApps.map((app) => ({
label: service.name, label: app.name,
value: service.name, value: app.name,
icon: service.appearance.iconUrl, icon: app.appearance.iconUrl,
url: service.behaviour.onClickUrl ?? service.url, url: app.behaviour.onClickUrl ?? app.url,
})); }));
const AutoCompleteItem = forwardRef<HTMLDivElement, any>( const AutoCompleteItem = forwardRef<HTMLDivElement, any>(
({ label, value, icon, url, ...others }: any, ref) => ( ({ label, value, icon, url, ...others }: any, ref) => (
<div ref={ref} {...others}> <div ref={ref} {...others}>
<SmallServiceItem service={{ label, value, icon, url }} /> <SmallAppItem app={{ label, value, icon, url }} />
</div> </div>
) )
); );

View File

@@ -0,0 +1,18 @@
import { Avatar, Group, Text } from '@mantine/core';
interface smallAppItem {
label: string;
icon?: string;
url?: string;
}
export default function SmallAppItem(props: any) {
const { app }: { app: smallAppItem } = props;
// TODO : Use Next/link
return (
<Group>
{app.icon && <Avatar src={app.icon} />}
<Text>{app.label}</Text>
</Group>
);
}

View File

@@ -52,10 +52,10 @@ export default function CalendarComponent(props: any) {
const [lidarrMedias, setLidarrMedias] = useState([] as any); const [lidarrMedias, setLidarrMedias] = useState([] as any);
const [radarrMedias, setRadarrMedias] = useState([] as any); const [radarrMedias, setRadarrMedias] = useState([] as any);
const [readarrMedias, setReadarrMedias] = useState([] as any); const [readarrMedias, setReadarrMedias] = useState([] as any);
const sonarrServices = config.services.filter((service) => service.type === 'Sonarr'); const sonarrServices = config.apps.filter((service) => service.type === 'Sonarr');
const radarrServices = config.services.filter((service) => service.type === 'Radarr'); const radarrServices = config.apps.filter((service) => service.type === 'Radarr');
const lidarrServices = config.services.filter((service) => service.type === 'Lidarr'); const lidarrServices = config.apps.filter((service) => service.type === 'Lidarr');
const readarrServices = config.services.filter((service) => service.type === 'Readarr'); const readarrServices = config.apps.filter((service) => service.type === 'Readarr');
const today = new Date(); const today = new Date();
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
@@ -125,7 +125,7 @@ export default function CalendarComponent(props: any) {
).then(() => { ).then(() => {
setReadarrMedias(currentReadarrMedias); setReadarrMedias(currentReadarrMedias);
}); });
}, [config.services]); }, [config.apps]);
const weekStartsAtSunday = const weekStartsAtSunday =
(config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false; (config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false;

View File

@@ -28,7 +28,7 @@ export interface IMedia {
export function OverseerrMediaDisplay(props: any) { export function OverseerrMediaDisplay(props: any) {
const { media }: { media: Result } = props; const { media }: { media: Result } = props;
const { config } = useConfig(); const { config } = useConfig();
const service = config.services.find( const service = config.apps.find(
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr' (service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
); );
@@ -58,7 +58,7 @@ export function ReadarrMediaDisplay(props: any) {
const { media }: { media: any } = props; const { media }: { media: any } = props;
const { config } = useConfig(); const { config } = useConfig();
// Find lidarr in services // Find lidarr in services
const readarr = config.services.find((service: serviceItem) => service.type === 'Readarr'); const readarr = config.apps.find((service: serviceItem) => service.type === 'Readarr');
// Find a poster CoverType // Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'cover'); const poster = media.images.find((image: any) => image.coverType === 'cover');
if (!readarr) { if (!readarr) {
@@ -90,7 +90,7 @@ export function LidarrMediaDisplay(props: any) {
const { media }: { media: any } = props; const { media }: { media: any } = props;
const { config } = useConfig(); const { config } = useConfig();
// Find lidarr in services // Find lidarr in services
const lidarr = config.services.find((service: serviceItem) => service.type === 'Lidarr'); const lidarr = config.apps.find((service: serviceItem) => service.type === 'Lidarr');
// Find a poster CoverType // Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'cover'); const poster = media.images.find((image: any) => image.coverType === 'cover');
if (!lidarr) { if (!lidarr) {

View File

@@ -137,7 +137,7 @@ export function DashdotComponent() {
const dashConfig = config.modules?.[DashdotModule.id].options as typeof DashdotModule['options']; const dashConfig = config.modules?.[DashdotModule.id].options as typeof DashdotModule['options'];
const isCompact = dashConfig?.useCompactView?.value ?? false; const isCompact = dashConfig?.useCompactView?.value ?? false;
const dashdotService: serviceItem | undefined = config.services.filter( const dashdotService: serviceItem | undefined = config.apps.filter(
(service) => service.type === 'Dash.' (service) => service.type === 'Dash.'
)[0]; )[0];
const dashdotUrl = dashdotService?.url ?? dashConfig?.url?.value ?? ''; const dashdotUrl = dashdotService?.url ?? dashConfig?.url?.value ?? '';

View File

@@ -20,7 +20,7 @@ import { v4 as uuidv4 } from 'uuid';
import { tryMatchService } from '../../tools/addToHomarr'; import { tryMatchService } from '../../tools/addToHomarr';
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions'; import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ServiceType } from '../../types/service'; import { AppType } from '../../types/app';
let t: TFunction<'modules/docker', undefined>; let t: TFunction<'modules/docker', undefined>;
@@ -163,7 +163,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
disabled={selected.length === 0 || selected.length > 1} disabled={selected.length === 0 || selected.length > 1}
onClick={() => { onClick={() => {
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`; const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
openContextModalGeneric<{ service: ServiceType }>({ openContextModalGeneric<{ service: AppType }>({
modal: 'editService', modal: 'editService',
innerProps: { innerProps: {
service: { service: {

View File

@@ -39,7 +39,7 @@ export const TorrentsModule: IModule = {
export default function TorrentsComponent() { export default function TorrentsComponent() {
const { config } = useConfig(); const { config } = useConfig();
const downloadServices = const downloadServices =
config.services.filter( config.apps.filter(
(service) => (service) =>
service.type === 'qBittorrent' || service.type === 'qBittorrent' ||
service.type === 'Transmission' || service.type === 'Transmission' ||

View File

@@ -30,7 +30,7 @@ export default function TotalDownloadsComponent() {
const setSafeInterval = useSetSafeInterval(); const setSafeInterval = useSetSafeInterval();
const { config } = useConfig(); const { config } = useConfig();
const downloadServices = const downloadServices =
config.services.filter( config.apps.filter(
(service) => (service) =>
service.type === 'qBittorrent' || service.type === 'qBittorrent' ||
service.type === 'Transmission' || service.type === 'Transmission' ||
@@ -68,7 +68,7 @@ export default function TotalDownloadsComponent() {
clearInterval(interval); clearInterval(interval);
}); });
}, 1000); }, 1000);
}, [config.services]); }, [config.apps]);
useEffect(() => { useEffect(() => {
torrentHistoryHandlers.append({ torrentHistoryHandlers.append({

View File

@@ -31,7 +31,7 @@ export const UsenetComponent: FunctionComponent = () => {
const { t } = useTranslation('modules/usenet'); const { t } = useTranslation('modules/usenet');
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id); const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
const { data } = useGetUsenetInfo({ serviceId: selectedServiceId! }); const { data } = useGetUsenetInfo({ appId: selectedServiceId! });
useEffect(() => { useEffect(() => {
if (!selectedServiceId && downloadServices.length) { if (!selectedServiceId && downloadServices.length) {
@@ -39,8 +39,8 @@ export const UsenetComponent: FunctionComponent = () => {
} }
}, [downloadServices, selectedServiceId]); }, [downloadServices, selectedServiceId]);
const { mutate: pause } = usePauseUsenetQueue({ serviceId: selectedServiceId! }); const { mutate: pause } = usePauseUsenetQueue({ appId: selectedServiceId! });
const { mutate: resume } = useResumeUsenetQueue({ serviceId: selectedServiceId! }); const { mutate: resume } = useResumeUsenetQueue({ appId: selectedServiceId! });
if (downloadServices.length === 0) { if (downloadServices.length === 0) {
return ( return (
@@ -98,10 +98,10 @@ export const UsenetComponent: FunctionComponent = () => {
/> />
)} )}
<Tabs.Panel value="queue"> <Tabs.Panel value="queue">
<UsenetQueueList serviceId={selectedServiceId} /> <UsenetQueueList appId={selectedServiceId} />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="history"> <Tabs.Panel value="history">
<UsenetHistoryList serviceId={selectedServiceId} /> <UsenetHistoryList appId={selectedServiceId} />
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
); );

View File

@@ -29,7 +29,7 @@ export async function getServerSideProps({
props: { props: {
config: { config: {
name: 'Default config', name: 'Default config',
services: [], apps: [],
settings: { settings: {
searchUrl: 'https://www.google.com/search?q=', searchUrl: 'https://www.google.com/search?q=',
}, },

View File

@@ -9,12 +9,12 @@ import { appWithTranslation } from 'next-i18next';
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
import { useState } from 'react'; import { useState } from 'react';
import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal';
import { ChangeIntegrationPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeIntegrationPositionModal'; import { ChangeIntegrationPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeIntegrationPositionModal';
import { ChangeServicePositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeServicePositionModal'; import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal';
import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal';
import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal'; import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal';
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
import { WidgetsEditModal } from '../components/Dashboard/Tiles/Widgets/WidgetsEditModal'; import { WidgetsEditModal } from '../components/Dashboard/Tiles/Widgets/WidgetsEditModal';
import { WidgetsRemoveModal } from '../components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal'; import { CategoryEditModal } from '../components/Dashboard/Wrappers/Category/CategoryEditModal';
import { ConfigProvider } from '../config/provider'; import { ConfigProvider } from '../config/provider';
import '../styles/global.scss'; import '../styles/global.scss';
@@ -86,12 +86,12 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
<NotificationsProvider limit={4} position="bottom-left"> <NotificationsProvider limit={4} position="bottom-left">
<ModalsProvider <ModalsProvider
modals={{ modals={{
editService: EditServiceModal, editApp: EditAppModal,
selectElement: SelectElementModal, selectElement: SelectElementModal,
integrationOptions: WidgetsEditModal, integrationOptions: WidgetsEditModal,
integrationRemove: WidgetsRemoveModal, integrationRemove: WidgetsRemoveModal,
categoryEditModal: CategoryEditModal, categoryEditModal: CategoryEditModal,
changeServicePositionModal: ChangeServicePositionModal, changeAppPositionModal: ChangeAppPositionModal,
changeIntegrationPositionModal: ChangeIntegrationPositionModal, changeIntegrationPositionModal: ChangeIntegrationPositionModal,
}} }}
> >

View File

@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { getConfig } from '../../../tools/config/getConfig'; import { getConfig } from '../../../tools/config/getConfig';
import { ServiceIntegrationType } from '../../../types/service'; import { AppIntegrationType } from '../../../types/app';
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the reuqest is a POST or a GET // Filter out if the reuqest is a POST or a GET
@@ -15,7 +15,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
}; };
async function Get(req: NextApiRequest, res: NextApiResponse) { async function Get(req: NextApiRequest, res: NextApiResponse) {
// Parse req.body as a ServiceItem // Parse req.body as a AppItem
const { const {
month: monthString, month: monthString,
year: yearString, year: yearString,
@@ -34,20 +34,20 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
const config = getConfig(configName); const config = getConfig(configName);
const mediaServiceIntegrationTypes: ServiceIntegrationType['type'][] = [ const mediaAppIntegrationTypes: AppIntegrationType['type'][] = [
'sonarr', 'sonarr',
'radarr', 'radarr',
'readarr', 'readarr',
'lidarr', 'lidarr',
]; ];
const mediaServices = config.services.filter( const mediaApps = config.apps.filter(
(service) => (app) =>
service.integration && mediaServiceIntegrationTypes.includes(service.integration.type) app.integration && mediaAppIntegrationTypes.includes(app.integration.type)
); );
const medias = await Promise.all( const medias = await Promise.all(
await mediaServices.map(async (service) => { await mediaApps.map(async (app) => {
const integration = service.integration!; const integration = app.integration!;
const endpoint = IntegrationTypeEndpointMap.get(integration.type); const endpoint = IntegrationTypeEndpointMap.get(integration.type);
if (!endpoint) { if (!endpoint) {
return { return {
@@ -57,7 +57,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
} }
// Get the origin URL // Get the origin URL
let { href: origin } = new URL(service.url); let { href: origin } = new URL(app.url);
if (origin.endsWith('/')) { if (origin.endsWith('/')) {
origin = origin.slice(0, -1); origin = origin.slice(0, -1);
} }
@@ -85,7 +85,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
}); });
} }
const IntegrationTypeEndpointMap = new Map<ServiceIntegrationType['type'], string>([ const IntegrationTypeEndpointMap = new Map<AppIntegrationType['type'], string>([
['sonarr', '/api/calendar'], ['sonarr', '/api/calendar'],
['radarr', '/api/v3/calendar'], ['radarr', '/api/v3/calendar'],
['lidarr', '/api/v1/calendar'], ['lidarr', '/api/v1/calendar'],

View File

@@ -11,8 +11,8 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
const { id, type } = req.query as { id: string; type: string }; const { id, type } = req.query as { id: string; type: string };
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
const service = config.services.find( const app = config.apps.find(
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr' (app) => app.type === 'Overseerr' || app.type === 'Jellyseerr'
); );
if (!id) { if (!id) {
return res.status(400).json({ error: 'No id provided' }); return res.status(400).json({ error: 'No id provided' });
@@ -20,18 +20,18 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
if (!type) { if (!type) {
return res.status(400).json({ error: 'No type provided' }); return res.status(400).json({ error: 'No type provided' });
} }
if (!service?.apiKey) { if (!app?.apiKey) {
return res.status(400).json({ error: 'No service found' }); return res.status(400).json({ error: 'No apps found' });
} }
const serviceUrl = new URL(service.url); const appUrl = new URL(app.url);
switch (type) { switch (type) {
case 'movie': case 'movie':
return axios return axios
.get(`${serviceUrl.origin}/api/v1/movie/${id}`, { .get(`${appUrl.origin}/api/v1/movie/${id}`, {
headers: { headers: {
// Set X-Api-Key to the value of the API key // Set X-Api-Key to the value of the API key
'X-Api-Key': service.apiKey, 'X-Api-Key': app.apiKey,
}, },
}) })
.then((axiosres) => res.status(200).json(axiosres.data)) .then((axiosres) => res.status(200).json(axiosres.data))
@@ -45,10 +45,10 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
case 'tv': case 'tv':
// Make request to the tv api // Make request to the tv api
return axios return axios
.get(`${serviceUrl.origin}/api/v1/tv/${id}`, { .get(`${appUrl.origin}/api/v1/tv/${id}`, {
headers: { headers: {
// Set X-Api-Key to the value of the API key // Set X-Api-Key to the value of the API key
'X-Api-Key': service.apiKey, 'X-Api-Key': app.apiKey,
}, },
}) })
.then((axiosres) => res.status(200).json(axiosres.data)) .then((axiosres) => res.status(200).json(axiosres.data))
@@ -72,8 +72,8 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
const { seasons, type } = req.body as { seasons?: number[]; type: MediaType }; const { seasons, type } = req.body as { seasons?: number[]; type: MediaType };
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
const service = config.services.find( const app = config.apps.find(
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr' (app) => app.type === 'Overseerr' || app.type === 'Jellyseerr'
); );
if (!id) { if (!id) {
return res.status(400).json({ error: 'No id provided' }); return res.status(400).json({ error: 'No id provided' });
@@ -81,13 +81,13 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
if (!type) { if (!type) {
return res.status(400).json({ error: 'No type provided' }); return res.status(400).json({ error: 'No type provided' });
} }
if (!service?.apiKey) { if (!app?.apiKey) {
return res.status(400).json({ error: 'No service found' }); return res.status(400).json({ error: 'No app found' });
} }
if (type === 'movie' && !seasons) { if (type === 'movie' && !seasons) {
return res.status(400).json({ error: 'No seasons provided' }); return res.status(400).json({ error: 'No seasons provided' });
} }
const serviceUrl = new URL(service.url); const appUrl = new URL(app.url);
Consola.info('Got an Overseerr request with these arguments', { Consola.info('Got an Overseerr request with these arguments', {
mediaType: type, mediaType: type,
mediaId: id, mediaId: id,
@@ -95,7 +95,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
}); });
return axios return axios
.post( .post(
`${serviceUrl.origin}/api/v1/request`, `${appUrl.origin}/api/v1/request`,
{ {
mediaType: type, mediaType: type,
mediaId: Number(id), mediaId: Number(id),
@@ -104,7 +104,7 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
{ {
headers: { headers: {
// Set X-Api-Key to the value of the API key // Set X-Api-Key to the value of the API key
'X-Api-Key': service.apiKey, 'X-Api-Key': app.apiKey,
}, },
} }
) )

View File

@@ -8,24 +8,24 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
const { query } = req.query; const { query } = req.query;
const service = config.services.find( const app = config.apps.find(
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr' (app) => app.type === 'Overseerr' || app.type === 'Jellyseerr'
); );
// If query is an empty string, return an empty array // If query is an empty string, return an empty array
if (query === '' || query === undefined) { if (query === '' || query === undefined) {
return res.status(200).json([]); return res.status(200).json([]);
} }
if (!service || !query || service === undefined || !service.apiKey) { if (!app || !query || app === undefined || !app.apiKey) {
return res.status(400).json({ return res.status(400).json({
error: 'Wrong request', error: 'Wrong request',
}); });
} }
const serviceUrl = new URL(service.url); const appUrl = new URL(app.url);
const data = await axios const data = await axios
.get(`${serviceUrl.origin}/api/v1/search?query=${query}`, { .get(`${appUrl.origin}/api/v1/search?query=${query}`, {
headers: { headers: {
// Set X-Api-Key to the value of the API key // Set X-Api-Key to the value of the API key
'X-Api-Key': service.apiKey, 'X-Api-Key': app.apiKey,
}, },
}) })
.then((res) => res.data); .then((res) => res.data);

View File

@@ -2,7 +2,7 @@ import ping from 'ping';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
async function Get(req: NextApiRequest, res: NextApiResponse) { async function Get(req: NextApiRequest, res: NextApiResponse) {
// Parse req.body as a ServiceItem // Parse req.body as a AppItem
const { url } = req.query; const { url } = req.query;
// Parse url as URL object // Parse url as URL object
const parsedUrl = new URL(url as string); const parsedUrl = new URL(url as string);

View File

@@ -8,50 +8,50 @@ import { getConfig } from '../../../tools/getConfig';
import { Config } from '../../../tools/types'; import { Config } from '../../../tools/types';
async function Post(req: NextApiRequest, res: NextApiResponse) { async function Post(req: NextApiRequest, res: NextApiResponse) {
// Get the type of service from the request url // Get the type of app from the request url
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
const qBittorrentServices = config.services.filter((service) => service.type === 'qBittorrent'); const qBittorrentApp = config.apps.filter((app) => app.type === 'qBittorrent');
const delugeServices = config.services.filter((service) => service.type === 'Deluge'); const delugeApp = config.apps.filter((app) => app.type === 'Deluge');
const transmissionServices = config.services.filter((service) => service.type === 'Transmission'); const transmissionApp = config.apps.filter((app) => app.type === 'Transmission');
const torrents: NormalizedTorrent[] = []; const torrents: NormalizedTorrent[] = [];
if (!qBittorrentServices && !delugeServices && !transmissionServices) { if (!qBittorrentApp && !delugeApp && !transmissionApp) {
return res.status(500).json({ return res.status(500).json({
statusCode: 500, statusCode: 500,
message: 'Missing services', message: 'Missing apps',
}); });
} }
try { try {
await Promise.all( await Promise.all(
qBittorrentServices.map((service) => qBittorrentApp.map((apps) =>
new QBittorrent({ new QBittorrent({
baseUrl: service.url, baseUrl: apps.url,
username: service.username, username: apps.username,
password: service.password, password: apps.password,
}) })
.getAllData() .getAllData()
.then((e) => torrents.push(...e.torrents)) .then((e) => torrents.push(...e.torrents))
) )
); );
await Promise.all( await Promise.all(
delugeServices.map((service) => delugeApp.map((apps) =>
new Deluge({ new Deluge({
baseUrl: service.url, baseUrl: apps.url,
password: 'password' in service ? service.password : '', password: 'password' in apps ? apps.password : '',
}) })
.getAllData() .getAllData()
.then((e) => torrents.push(...e.torrents)) .then((e) => torrents.push(...e.torrents))
) )
); );
// Map transmissionServices // Map transmissionApps
await Promise.all( await Promise.all(
transmissionServices.map((service) => transmissionApp.map((apps) =>
new Transmission({ new Transmission({
baseUrl: service.url, baseUrl: apps.url,
username: 'username' in service ? service.username : '', username: 'username' in apps ? apps.username : '',
password: 'password' in service ? service.password : '', password: 'password' in apps ? apps.password : '',
}) })
.getAllData() .getAllData()
.then((e) => torrents.push(...e.torrents)) .then((e) => torrents.push(...e.torrents))

View File

@@ -11,7 +11,7 @@ import { UsenetHistoryItem } from '../../../../components/Dashboard/Tiles/UseNet
dayjs.extend(duration); dayjs.extend(duration);
export interface UsenetHistoryRequestParams { export interface UsenetHistoryRequestParams {
serviceId: string; appId: string;
offset: number; offset: number;
limit: number; limit: number;
} }
@@ -25,25 +25,25 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
try { try {
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const config = getConfig(configName?.toString() ?? 'default'); const config = getConfig(configName?.toString() ?? 'default');
const { limit, offset, serviceId } = req.query as any as UsenetHistoryRequestParams; const { limit, offset, appId } = req.query as any as UsenetHistoryRequestParams;
const service = config.services.find((x) => x.id === serviceId); const app = config.apps.find((x) => x.id === appId);
if (!service) { if (!app) {
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); throw new Error(`App with ID "${req.query.appId}" could not be found.`);
} }
let response: UsenetHistoryResponse; let response: UsenetHistoryResponse;
switch (service.integration?.type) { switch (app.integration?.type) {
case 'nzbGet': { case 'nzbGet': {
const url = new URL(service.url); const url = new URL(app.url);
const options = { const options = {
host: url.hostname, host: url.hostname,
port: url.port, port: url.port,
login: login:
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
hash: hash:
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
}; };
const nzbGet = NzbgetClient(options); const nzbGet = NzbgetClient(options);
@@ -77,11 +77,11 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
case 'sabnzbd': { case 'sabnzbd': {
const { origin } = new URL(service.url); const { origin } = new URL(app.url);
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value; const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) { if (!apiKey) {
throw new Error(`API Key for service "${service.name}" is missing`); throw new Error(`API Key for app "${app.name}" is missing`);
} }
const history = await new Client(origin, apiKey).history(offset, limit); const history = await new Client(origin, apiKey).history(offset, limit);
@@ -100,7 +100,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
default: default:
throw new Error(`Service type "${service.integration?.type}" unrecognized.`); throw new Error(`App type "${app.integration?.type}" unrecognized.`);
} }
return res.status(200).json(response); return res.status(200).json(response);

View File

@@ -3,16 +3,14 @@ import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'; import duration from 'dayjs/plugin/duration';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { Client } from 'sabnzbd-api'; import { Client } from 'sabnzbd-api';
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
import { Config } from '../../../../tools/types';
import { NzbgetStatus } from './nzbget/types';
import { NzbgetClient } from './nzbget/nzbget-client';
import { getConfig } from '../../../../tools/config/getConfig'; import { getConfig } from '../../../../tools/config/getConfig';
import { NzbgetClient } from './nzbget/nzbget-client';
import { NzbgetStatus } from './nzbget/types';
dayjs.extend(duration); dayjs.extend(duration);
export interface UsenetInfoRequestParams { export interface UsenetInfoRequestParams {
serviceId: string; appId: string;
} }
export interface UsenetInfoResponse { export interface UsenetInfoResponse {
@@ -26,25 +24,25 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
try { try {
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const config = getConfig(configName?.toString() ?? 'default'); const config = getConfig(configName?.toString() ?? 'default');
const { serviceId } = req.query as any as UsenetInfoRequestParams; const { appId } = req.query as any as UsenetInfoRequestParams;
const service = config.services.find((x) => x.id === serviceId); const app = config.apps.find((x) => x.id === appId);
if (!service) { if (!app) {
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); throw new Error(`App with ID "${req.query.appId}" could not be found.`);
} }
let response: UsenetInfoResponse; let response: UsenetInfoResponse;
switch (service.integration?.type) { switch (app.integration?.type) {
case 'nzbGet': { case 'nzbGet': {
const url = new URL(service.url); const url = new URL(app.url);
const options = { const options = {
host: url.hostname, host: url.hostname,
port: url.port, port: url.port,
login: login:
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
hash: hash:
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
}; };
const nzbGet = NzbgetClient(options); const nzbGet = NzbgetClient(options);
@@ -74,12 +72,12 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
case 'sabnzbd': { case 'sabnzbd': {
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value; const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) { if (!apiKey) {
throw new Error(`API Key for service "${service.name}" is missing`); throw new Error(`API Key for app "${app.name}" is missing`);
} }
const { origin } = new URL(service.url); const { origin } = new URL(app.url);
const queue = await new Client(origin, apiKey).queue(0, -1); const queue = await new Client(origin, apiKey).queue(0, -1);
@@ -99,7 +97,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
default: default:
throw new Error(`Service type "${service.integration?.type}" unrecognized.`); throw new Error(`App type "${app.integration?.type}" unrecognized.`);
} }
return res.status(200).json(response); return res.status(200).json(response);

View File

@@ -3,19 +3,19 @@ import { NzbgetClientOptions } from './types';
export function NzbgetClient(options: NzbgetClientOptions) { export function NzbgetClient(options: NzbgetClientOptions) {
if (!options?.host) { if (!options?.host) {
throw new Error('Cannot connect to NZBGet. Missing host in service config.'); throw new Error('Cannot connect to NZBGet. Missing host in app config.');
} }
if (!options?.port) { if (!options?.port) {
throw new Error('Cannot connect to NZBGet. Missing port in service config.'); throw new Error('Cannot connect to NZBGet. Missing port in app config.');
} }
if (!options?.login) { if (!options?.login) {
throw new Error('Cannot connect to NZBGet. Missing username in service config.'); throw new Error('Cannot connect to NZBGet. Missing username in app config.');
} }
if (!options?.hash) { if (!options?.hash) {
throw new Error('Cannot connect to NZBGet. Missing password in service config.'); throw new Error('Cannot connect to NZBGet. Missing password in app config.');
} }
return new NZBGet(options); return new NZBGet(options);

View File

@@ -9,32 +9,32 @@ import { NzbgetClient } from './nzbget/nzbget-client';
dayjs.extend(duration); dayjs.extend(duration);
export interface UsenetPauseRequestParams { export interface UsenetPauseRequestParams {
serviceId: string; appId: string;
} }
async function Post(req: NextApiRequest, res: NextApiResponse) { async function Post(req: NextApiRequest, res: NextApiResponse) {
try { try {
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const config = getConfig(configName?.toString() ?? 'default'); const config = getConfig(configName?.toString() ?? 'default');
const { serviceId } = req.query as any as UsenetPauseRequestParams; const { appId } = req.query as any as UsenetPauseRequestParams;
const service = config.services.find((x) => x.id === serviceId); const app = config.apps.find((x) => x.id === appId);
if (!service) { if (!app) {
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); throw new Error(`App with ID "${req.query.appId}" could not be found.`);
} }
let result; let result;
switch (service.integration?.type) { switch (app.integration?.type) {
case 'nzbGet': { case 'nzbGet': {
const url = new URL(service.url); const url = new URL(app.url);
const options = { const options = {
host: url.hostname, host: url.hostname,
port: url.port, port: url.port,
login: login:
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
hash: hash:
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
}; };
const nzbGet = NzbgetClient(options); const nzbGet = NzbgetClient(options);
@@ -51,18 +51,18 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
case 'sabnzbd': { case 'sabnzbd': {
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value; const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) { if (!apiKey) {
throw new Error(`API Key for service "${service.name}" is missing`); throw new Error(`API Key for app "${app.name}" is missing`);
} }
const { origin } = new URL(service.url); const { origin } = new URL(app.url);
result = await new Client(origin, apiKey).queuePause(); result = await new Client(origin, apiKey).queuePause();
break; break;
} }
default: default:
throw new Error(`Service type "${service.integration?.type}" unrecognized.`); throw new Error(`App type "${app.integration?.type}" unrecognized.`);
} }
return res.status(200).json(result); return res.status(200).json(result);

View File

@@ -11,7 +11,7 @@ import { NzbgetQueueItem, NzbgetStatus } from './nzbget/types';
dayjs.extend(duration); dayjs.extend(duration);
export interface UsenetQueueRequestParams { export interface UsenetQueueRequestParams {
serviceId: string; appId: string;
offset: number; offset: number;
limit: number; limit: number;
} }
@@ -25,25 +25,25 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
try { try {
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const config = getConfig(configName?.toString() ?? 'default'); const config = getConfig(configName?.toString() ?? 'default');
const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams; const { limit, offset, appId } = req.query as any as UsenetQueueRequestParams;
const service = config.services.find((x) => x.id === serviceId); const app = config.apps.find((x) => x.id === appId);
if (!service) { if (!app) {
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); throw new Error(`App with ID "${req.query.appId}" could not be found.`);
} }
let response: UsenetQueueResponse; let response: UsenetQueueResponse;
switch (service.integration?.type) { switch (app.integration?.type) {
case 'nzbGet': { case 'nzbGet': {
const url = new URL(service.url); const url = new URL(app.url);
const options = { const options = {
host: url.hostname, host: url.hostname,
port: url.port, port: url.port,
login: login:
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
hash: hash:
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
}; };
const nzbGet = NzbgetClient(options); const nzbGet = NzbgetClient(options);
@@ -93,12 +93,12 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
case 'sabnzbd': { case 'sabnzbd': {
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value; const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) { if (!apiKey) {
throw new Error(`API Key for service "${service.name}" is missing`); throw new Error(`API Key for app "${app.name}" is missing`);
} }
const { origin } = new URL(service.url); const { origin } = new URL(app.url);
const queue = await new Client(origin, apiKey).queue(offset, limit); const queue = await new Client(origin, apiKey).queue(offset, limit);
const items: UsenetQueueItem[] = queue.slots.map((slot) => { const items: UsenetQueueItem[] = queue.slots.map((slot) => {
@@ -126,7 +126,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
default: default:
throw new Error(`Service type "${service.integration?.type}" unrecognized.`); throw new Error(`App type "${app.integration?.type}" unrecognized.`);
} }
return res.status(200).json(response); return res.status(200).json(response);

View File

@@ -4,14 +4,12 @@ import duration from 'dayjs/plugin/duration';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { Client } from 'sabnzbd-api'; import { Client } from 'sabnzbd-api';
import { getConfig } from '../../../../tools/config/getConfig'; import { getConfig } from '../../../../tools/config/getConfig';
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
import { Config } from '../../../../tools/types';
import { NzbgetClient } from './nzbget/nzbget-client'; import { NzbgetClient } from './nzbget/nzbget-client';
dayjs.extend(duration); dayjs.extend(duration);
export interface UsenetResumeRequestParams { export interface UsenetResumeRequestParams {
serviceId: string; appId: string;
nzbId?: string; nzbId?: string;
} }
@@ -19,25 +17,25 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
try { try {
const configName = getCookie('config-name', { req }); const configName = getCookie('config-name', { req });
const config = getConfig(configName?.toString() ?? 'default'); const config = getConfig(configName?.toString() ?? 'default');
const { serviceId } = req.query as any as UsenetResumeRequestParams; const { appId } = req.query as any as UsenetResumeRequestParams;
const service = config.services.find((x) => x.id === serviceId); const app = config.apps.find((x) => x.id === appId);
if (!service) { if (!app) {
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); throw new Error(`App with ID "${req.query.appId}" could not be found.`);
} }
let result; let result;
switch (service.integration?.type) { switch (app.integration?.type) {
case 'nzbGet': { case 'nzbGet': {
const url = new URL(service.url); const url = new URL(app.url);
const options = { const options = {
host: url.hostname, host: url.hostname,
port: url.port, port: url.port,
login: login:
service.integration.properties.find((x) => x.field === 'username')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
hash: hash:
service.integration.properties.find((x) => x.field === 'password')?.value ?? undefined, app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
}; };
const nzbGet = NzbgetClient(options); const nzbGet = NzbgetClient(options);
@@ -54,18 +52,18 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
case 'sabnzbd': { case 'sabnzbd': {
const apiKey = service.integration.properties.find((x) => x.field === 'apiKey')?.value; const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) { if (!apiKey) {
throw new Error(`API Key for service "${service.name}" is missing`); throw new Error(`API Key for app "${app.name}" is missing`);
} }
const { origin } = new URL(service.url); const { origin } = new URL(app.url);
result = await new Client(origin, apiKey).queueResume(); result = await new Client(origin, apiKey).queueResume();
break; break;
} }
default: default:
throw new Error(`Service type "${service.integration?.type}" unrecognized.`); throw new Error(`App type "${app.integration?.type}" unrecognized.`);
} }
return res.status(200).json(result); return res.status(200).json(result);

View File

@@ -43,8 +43,8 @@ export default async function addToHomarr(
) { ) {
setConfig({ setConfig({
...config, ...config,
services: [ apps: [
...config.services, ...config.apps,
{ {
name: container.Names[0].substring(1), name: container.Names[0].substring(1),
id: container.Id, id: container.Id,

View File

@@ -7,7 +7,7 @@ export const getFallbackConfig = (name?: string): BackendConfigType => ({
}, },
categories: [], categories: [],
integrations: {}, integrations: {},
services: [], apps: [],
settings: { settings: {
common: { common: {
searchEngine: { searchEngine: {

View File

@@ -6,7 +6,7 @@ export const getFrontendConfig = (name: string): ConfigType => {
return { return {
...config, ...config,
services: config.services.map((s) => ({ apps: config.apps.map((s) => ({
...s, ...s,
integration: s.integration integration: s.integration
? { ? {

View File

@@ -18,7 +18,7 @@ const POLLING_INTERVAL = 2000;
export const useGetUsenetInfo = (params: UsenetInfoRequestParams) => export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
useQuery( useQuery(
['usenetInfo', params.serviceId], ['usenetInfo', params.appId],
async () => async () =>
( (
await axios.get<UsenetInfoResponse>('/api/modules/usenet', { await axios.get<UsenetInfoResponse>('/api/modules/usenet', {
@@ -29,7 +29,7 @@ export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
refetchInterval: POLLING_INTERVAL, refetchInterval: POLLING_INTERVAL,
keepPreviousData: true, keepPreviousData: true,
retry: 2, retry: 2,
enabled: !!params.serviceId, enabled: !!params.appId,
} }
); );
@@ -80,14 +80,14 @@ export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) =>
).data, ).data,
{ {
async onMutate() { async onMutate() {
await queryClient.cancelQueries(['usenetInfo', params.serviceId]); await queryClient.cancelQueries(['usenetInfo', params.appId]);
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([ const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
'usenetInfo', 'usenetInfo',
params.serviceId, params.appId,
]); ]);
if (previousInfo) { if (previousInfo) {
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.serviceId], { queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.appId], {
...previousInfo, ...previousInfo,
paused: true, paused: true,
}); });
@@ -98,13 +98,13 @@ export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) =>
onError(err, _, context) { onError(err, _, context) {
if (context?.previousInfo) { if (context?.previousInfo) {
queryClient.setQueryData<UsenetInfoResponse>( queryClient.setQueryData<UsenetInfoResponse>(
['usenetInfo', params.serviceId], ['usenetInfo', params.appId],
context.previousInfo context.previousInfo
); );
} }
}, },
onSettled() { onSettled() {
queryClient.invalidateQueries(['usenetInfo', params.serviceId]); queryClient.invalidateQueries(['usenetInfo', params.appId]);
}, },
} }
); );
@@ -124,14 +124,14 @@ export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) =>
).data, ).data,
{ {
async onMutate() { async onMutate() {
await queryClient.cancelQueries(['usenetInfo', params.serviceId]); await queryClient.cancelQueries(['usenetInfo', params.appId]);
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([ const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
'usenetInfo', 'usenetInfo',
params.serviceId, params.appId,
]); ]);
if (previousInfo) { if (previousInfo) {
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.serviceId], { queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.appId], {
...previousInfo, ...previousInfo,
paused: false, paused: false,
}); });
@@ -142,13 +142,13 @@ export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) =>
onError(err, _, context) { onError(err, _, context) {
if (context?.previousInfo) { if (context?.previousInfo) {
queryClient.setQueryData<UsenetInfoResponse>( queryClient.setQueryData<UsenetInfoResponse>(
['usenetInfo', params.serviceId], ['usenetInfo', params.appId],
context.previousInfo context.previousInfo
); );
} }
}, },
onSettled() { onSettled() {
queryClient.invalidateQueries(['usenetInfo', params.serviceId]); queryClient.invalidateQueries(['usenetInfo', params.appId]);
}, },
} }
); );

View File

@@ -8,7 +8,7 @@ export const useGetServiceByType = (...serviceTypes: ServiceType[]) => {
}; };
export const getServiceByType = (config: Config, ...serviceTypes: ServiceType[]) => export const getServiceByType = (config: Config, ...serviceTypes: ServiceType[]) =>
config.services.filter((s) => serviceTypes.includes(s.type)); config.apps.filter((s) => serviceTypes.includes(s.type));
export const getServiceById = (config: Config, id: string) => export const getServiceById = (config: Config, id: string) =>
config.services.find((s) => s.id === id); config.apps.find((s) => s.id === id);

View File

@@ -3,12 +3,12 @@ import { Config } from './types';
export function migrateToIdConfig(config: Config): Config { export function migrateToIdConfig(config: Config): Config {
// Set the config and add an ID to all the services that don't have one // Set the config and add an ID to all the services that don't have one
const services = config.services.map((service) => ({ const services = config.apps.map((service) => ({
...service, ...service,
id: service.id ?? uuidv4(), id: service.id ?? uuidv4(),
})); }));
return { return {
...config, ...config,
services, apps: services,
}; };
} }

View File

@@ -15,7 +15,7 @@ type configContextType = {
const configContext = createContext<configContextType>({ const configContext = createContext<configContextType>({
config: { config: {
name: 'default', name: 'default',
services: [], apps: [],
settings: { settings: {
searchUrl: 'https://google.com/search?q=', searchUrl: 'https://google.com/search?q=',
}, },
@@ -41,7 +41,7 @@ type Props = {
export function ConfigProvider({ children }: Props) { export function ConfigProvider({ children }: Props) {
const [config, setConfigInternal] = useState<Config>({ const [config, setConfigInternal] = useState<Config>({
name: 'default', name: 'default',
services: [], apps: [],
settings: { settings: {
searchUrl: 'https://www.google.com/search?q=', searchUrl: 'https://www.google.com/search?q=',
}, },

View File

@@ -20,7 +20,7 @@ export interface Settings {
export interface Config { export interface Config {
name: string; name: string;
services: serviceItem[]; apps: serviceItem[];
settings: Settings; settings: Settings;
modules: { modules: {
[key: string]: ConfigModule; [key: string]: ConfigModule;

View File

@@ -1,31 +1,31 @@
import { IconKey, IconPassword, IconUser, TablerIcon } from '@tabler/icons'; import { IconKey, IconPassword, IconUser, TablerIcon } from '@tabler/icons';
import { TileBaseType } from './tile'; import { TileBaseType } from './tile';
export interface ServiceType extends TileBaseType { export interface AppType extends TileBaseType {
id: string; id: string;
name: string; name: string;
url: string; url: string;
behaviour: ServiceBehaviourType; behaviour: AppBehaviourType;
network: ServiceNetworkType; network: AppNetworkType;
appearance: ServiceAppearanceType; appearance: AppAppearanceType;
integration: ServiceIntegrationType; integration: AppIntegrationType;
} }
export type ConfigServiceType = Omit<ServiceType, 'integration'> & { export type ConfigAppType = Omit<AppType, 'integration'> & {
integration?: ConfigServiceIntegrationType | null; integration?: ConfigAppIntegrationType | null;
}; };
interface ServiceBehaviourType { interface AppBehaviourType {
onClickUrl: string; onClickUrl: string;
isOpeningNewTab: boolean; isOpeningNewTab: boolean;
} }
interface ServiceNetworkType { interface AppNetworkType {
enabledStatusChecker: boolean; enabledStatusChecker: boolean;
okStatus: number[]; okStatus: number[];
} }
interface ServiceAppearanceType { interface AppAppearanceType {
iconUrl: string; iconUrl: string;
} }
@@ -42,28 +42,28 @@ export type IntegrationType =
| 'transmission' | 'transmission'
| 'nzbGet'; | 'nzbGet';
export type ServiceIntegrationType = { export type AppIntegrationType = {
type: IntegrationType | null; type: IntegrationType | null;
properties: ServiceIntegrationPropertyType[]; properties: AppIntegrationPropertyType[];
}; };
export type ConfigServiceIntegrationType = Omit<ServiceIntegrationType, 'properties'> & { export type ConfigAppIntegrationType = Omit<AppIntegrationType, 'properties'> & {
properties: ConfigServiceIntegrationPropertyType[]; properties: ConfigAppIntegrationPropertyType[];
}; };
export type ServiceIntegrationPropertyType = { export type AppIntegrationPropertyType = {
type: 'private' | 'public'; type: 'private' | 'public';
field: IntegrationField; field: IntegrationField;
value?: string | undefined; value?: string | undefined;
isDefined: boolean; isDefined: boolean;
}; };
type ConfigServiceIntegrationPropertyType = Omit<ServiceIntegrationPropertyType, 'isDefined'>; type ConfigAppIntegrationPropertyType = Omit<AppIntegrationPropertyType, 'isDefined'>;
export type IntegrationField = 'apiKey' | 'password' | 'username'; export type IntegrationField = 'apiKey' | 'password' | 'username';
export const integrationFieldProperties: { export const integrationFieldProperties: {
[key in ServiceIntegrationType['type']]: IntegrationField[]; [key in AppIntegrationType['type']]: IntegrationField[];
} = { } = {
lidarr: ['apiKey'], lidarr: ['apiKey'],
radarr: ['apiKey'], radarr: ['apiKey'],

View File

@@ -1,6 +1,6 @@
import { CategoryType } from './category'; import { CategoryType } from './category';
import { WrapperType } from './wrapper'; import { WrapperType } from './wrapper';
import { ConfigServiceType, ServiceType } from './service'; import { ConfigAppType, AppType } from './app';
import { IntegrationsType } from './integration'; import { IntegrationsType } from './integration';
import { SettingsType } from './settings'; import { SettingsType } from './settings';
@@ -9,13 +9,13 @@ export interface ConfigType {
configProperties: ConfigPropertiesType; configProperties: ConfigPropertiesType;
categories: CategoryType[]; categories: CategoryType[];
wrappers: WrapperType[]; wrappers: WrapperType[];
services: ServiceType[]; apps: AppType[];
integrations: IntegrationsType; integrations: IntegrationsType;
settings: SettingsType; settings: SettingsType;
} }
export type BackendConfigType = Omit<ConfigType, 'services'> & { export type BackendConfigType = Omit<ConfigType, 'apps'> & {
services: ConfigServiceType[]; apps: ConfigAppType[];
}; };
export interface ConfigPropertiesType { export interface ConfigPropertiesType {

View File

@@ -60,7 +60,7 @@ export const DashDotTile = ({ module, className }: DashDotTileProps) => {
{menu} {menu}
<div> <div>
{heading} {heading}
<p>{t('card.errors.noService')}</p> <p>{t('card.errors.noApp')}</p>
</div> </div>
</HomarrCardWrapper> </HomarrCardWrapper>
); );

View File

@@ -19,7 +19,7 @@ import { useTranslation } from 'next-i18next';
import { UsenetQueueList } from './UsenetQueueList'; import { UsenetQueueList } from './UsenetQueueList';
import { UsenetHistoryList } from './UsenetHistoryList'; import { UsenetHistoryList } from './UsenetHistoryList';
import { BaseTileProps } from '../../components/Dashboard/Tiles/type'; import { BaseTileProps } from '../../components/Dashboard/Tiles/type';
import { ServiceIntegrationType } from '../../types/service'; import { AppIntegrationType } from '../../types/app';
import { useConfigContext } from '../../config/provider'; import { useConfigContext } from '../../config/provider';
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api'; import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
import { HomarrCardWrapper } from '../../components/Dashboard/Tiles/HomarrCardWrapper'; import { HomarrCardWrapper } from '../../components/Dashboard/Tiles/HomarrCardWrapper';
@@ -27,31 +27,31 @@ import { humanFileSize } from '../../tools/humanFileSize';
dayjs.extend(duration); dayjs.extend(duration);
const downloadServiceTypes: ServiceIntegrationType['type'][] = ['sabnzbd', 'nzbGet']; const downloadAppTypes: AppIntegrationType['type'][] = ['sabnzbd', 'nzbGet'];
interface UseNetTileProps extends BaseTileProps {} interface UseNetTileProps extends BaseTileProps {}
export const UseNetTile = ({ className }: UseNetTileProps) => { export const UseNetTile = ({ className }: UseNetTileProps) => {
const { t } = useTranslation('modules/usenet'); const { t } = useTranslation('modules/usenet');
const { config } = useConfigContext(); const { config } = useConfigContext();
const downloadServices = const downloadApps =
config?.services.filter( config?.apps.filter(
(x) => x.integration && downloadServiceTypes.includes(x.integration.type) (x) => x.integration && downloadAppTypes.includes(x.integration.type)
) ?? []; ) ?? [];
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id); const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
const { data } = useGetUsenetInfo({ serviceId: selectedServiceId! }); const { data } = useGetUsenetInfo({ appId: selectedAppId! });
useEffect(() => { useEffect(() => {
if (!selectedServiceId && downloadServices.length) { if (!selectedAppId && downloadApps.length) {
setSelectedService(downloadServices[0].id); setSelectedApp(downloadApps[0].id);
} }
}, [downloadServices, selectedServiceId]); }, [downloadApps, selectedAppId]);
const { mutate: pause } = usePauseUsenetQueue({ serviceId: selectedServiceId! }); const { mutate: pause } = usePauseUsenetQueue({ appId: selectedAppId! });
const { mutate: resume } = useResumeUsenetQueue({ serviceId: selectedServiceId! }); const { mutate: resume } = useResumeUsenetQueue({ appId: selectedAppId! });
if (downloadServices.length === 0) { if (downloadApps.length === 0) {
return ( return (
<HomarrCardWrapper className={className}> <HomarrCardWrapper className={className}>
<Stack> <Stack>
@@ -64,7 +64,7 @@ export const UseNetTile = ({ className }: UseNetTileProps) => {
); );
} }
if (!selectedServiceId) { if (!selectedAppId) {
return null; return null;
} }
@@ -90,16 +90,16 @@ export const UseNetTile = ({ className }: UseNetTileProps) => {
</Group> </Group>
)} )}
</Tabs.List> </Tabs.List>
{downloadServices.length > 1 && ( {downloadApps.length > 1 && (
<Select <Select
value={selectedServiceId} value={selectedAppId}
onChange={setSelectedService} onChange={setSelectedApp}
ml="xs" ml="xs"
data={downloadServices.map((service) => ({ value: service.id, label: service.name }))} data={downloadApps.map((app) => ({ value: app.id, label: app.name }))}
/> />
)} )}
<Tabs.Panel value="queue"> <Tabs.Panel value="queue">
<UsenetQueueList serviceId={selectedServiceId} /> <UsenetQueueList appId={selectedAppId} />
{!data ? null : data.paused ? ( {!data ? null : data.paused ? (
<Button uppercase onClick={() => resume()} radius="xl" size="xs" fullWidth mt="sm"> <Button uppercase onClick={() => resume()} radius="xl" size="xs" fullWidth mt="sm">
<IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')} <IconPlayerPlay size={12} style={{ marginRight: 5 }} /> {t('info.paused')}
@@ -112,7 +112,7 @@ export const UseNetTile = ({ className }: UseNetTileProps) => {
)} )}
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="history" style={{ display: 'flex', flexDirection: 'column' }}> <Tabs.Panel value="history" style={{ display: 'flex', flexDirection: 'column' }}>
<UsenetHistoryList serviceId={selectedServiceId} /> <UsenetHistoryList appId={selectedAppId} />
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
</HomarrCardWrapper> </HomarrCardWrapper>

View File

@@ -25,12 +25,12 @@ import { parseDuration } from '../../tools/parseDuration';
dayjs.extend(duration); dayjs.extend(duration);
interface UsenetHistoryListProps { interface UsenetHistoryListProps {
serviceId: string; appId: string;
} }
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ serviceId }) => { export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ appId }) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { t } = useTranslation(['modules/usenet', 'common']); const { t } = useTranslation(['modules/usenet', 'common']);
@@ -39,7 +39,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ s
const { data, isLoading, isError, error } = useGetUsenetHistory({ const { data, isLoading, isError, error } = useGetUsenetHistory({
limit: PAGE_SIZE, limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE, offset: (page - 1) * PAGE_SIZE,
serviceId, appId: appId,
}); });
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE); const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);

View File

@@ -27,12 +27,12 @@ import { humanFileSize } from '../../tools/humanFileSize';
dayjs.extend(duration); dayjs.extend(duration);
interface UsenetQueueListProps { interface UsenetQueueListProps {
serviceId: string; appId: string;
} }
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ serviceId }) => { export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId }) => {
const theme = useMantineTheme(); const theme = useMantineTheme();
const { t } = useTranslation('modules/usenet'); const { t } = useTranslation('modules/usenet');
const progressbarBreakpoint = theme.breakpoints.xs; const progressbarBreakpoint = theme.breakpoints.xs;
@@ -44,7 +44,7 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ servi
const { data, isLoading, isError, error } = useGetUsenetDownloads({ const { data, isLoading, isError, error } = useGetUsenetDownloads({
limit: PAGE_SIZE, limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE, offset: (page - 1) * PAGE_SIZE,
serviceId, appId: appId,
}); });
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE); const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);

View File

@@ -1114,7 +1114,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/carousel@npm:^5.9.0": "@mantine/carousel@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/carousel@npm:5.9.3" resolution: "@mantine/carousel@npm:5.9.3"
dependencies: dependencies:
@@ -1128,7 +1128,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/core@npm:^5.9.0": "@mantine/core@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/core@npm:5.9.3" resolution: "@mantine/core@npm:5.9.3"
dependencies: dependencies:
@@ -1145,7 +1145,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/dates@npm:^5.9.0": "@mantine/dates@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/dates@npm:5.9.3" resolution: "@mantine/dates@npm:5.9.3"
dependencies: dependencies:
@@ -1159,7 +1159,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/dropzone@npm:^5.9.0": "@mantine/dropzone@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/dropzone@npm:5.9.3" resolution: "@mantine/dropzone@npm:5.9.3"
dependencies: dependencies:
@@ -1174,7 +1174,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/form@npm:^5.9.0": "@mantine/form@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/form@npm:5.9.3" resolution: "@mantine/form@npm:5.9.3"
dependencies: dependencies:
@@ -1186,7 +1186,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/hooks@npm:^5.9.0": "@mantine/hooks@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/hooks@npm:5.9.3" resolution: "@mantine/hooks@npm:5.9.3"
peerDependencies: peerDependencies:
@@ -1195,7 +1195,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/modals@npm:^5.9.0": "@mantine/modals@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/modals@npm:5.9.3" resolution: "@mantine/modals@npm:5.9.3"
dependencies: dependencies:
@@ -1209,7 +1209,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/next@npm:^5.9.0": "@mantine/next@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/next@npm:5.9.3" resolution: "@mantine/next@npm:5.9.3"
dependencies: dependencies:
@@ -1223,7 +1223,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/notifications@npm:^5.9.0": "@mantine/notifications@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/notifications@npm:5.9.3" resolution: "@mantine/notifications@npm:5.9.3"
dependencies: dependencies:
@@ -1238,7 +1238,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/prism@npm:^5.9.0": "@mantine/prism@npm:^5.9.3":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/prism@npm:5.9.3" resolution: "@mantine/prism@npm:5.9.3"
dependencies: dependencies:
@@ -4536,7 +4536,7 @@ __metadata:
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>": "fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
version: 2.3.2 version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7" resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
dependencies: dependencies:
node-gyp: latest node-gyp: latest
conditions: os=darwin conditions: os=darwin
@@ -4900,16 +4900,16 @@ __metadata:
"@dnd-kit/utilities": ^3.2.0 "@dnd-kit/utilities": ^3.2.0
"@emotion/react": ^11.10.5 "@emotion/react": ^11.10.5
"@emotion/server": ^11.10.0 "@emotion/server": ^11.10.0
"@mantine/carousel": ^5.9.0 "@mantine/carousel": ^5.9.3
"@mantine/core": ^5.9.0 "@mantine/core": ^5.9.3
"@mantine/dates": ^5.9.0 "@mantine/dates": ^5.9.3
"@mantine/dropzone": ^5.9.0 "@mantine/dropzone": ^5.9.3
"@mantine/form": ^5.9.0 "@mantine/form": ^5.9.3
"@mantine/hooks": ^5.9.0 "@mantine/hooks": ^5.9.3
"@mantine/modals": ^5.9.0 "@mantine/modals": ^5.9.3
"@mantine/next": ^5.9.0 "@mantine/next": ^5.9.3
"@mantine/notifications": ^5.9.0 "@mantine/notifications": ^5.9.3
"@mantine/prism": ^5.9.0 "@mantine/prism": ^5.9.3
"@next/bundle-analyzer": ^12.1.4 "@next/bundle-analyzer": ^12.1.4
"@next/eslint-plugin-next": ^12.1.4 "@next/eslint-plugin-next": ^12.1.4
"@nivo/core": ^0.79.0 "@nivo/core": ^0.79.0
@@ -7489,7 +7489,7 @@ __metadata:
"resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>": "resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>":
version: 1.22.1 version: 1.22.1
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=07638b" resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=c3c19d"
dependencies: dependencies:
is-core-module: ^2.9.0 is-core-module: ^2.9.0
path-parse: ^1.0.7 path-parse: ^1.0.7
@@ -7502,7 +7502,7 @@ __metadata:
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>": "resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
version: 2.0.0-next.4 version: 2.0.0-next.4
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=07638b" resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=c3c19d"
dependencies: dependencies:
is-core-module: ^2.9.0 is-core-module: ^2.9.0
path-parse: ^1.0.7 path-parse: ^1.0.7
@@ -8338,7 +8338,7 @@ __metadata:
"typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>": "typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>":
version: 4.9.4 version: 4.9.4
resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=7ad353" resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=d73830"
bin: bin:
tsc: bin/tsc tsc: bin/tsc
tsserver: bin/tsserver tsserver: bin/tsserver