diff --git a/data/constants.ts b/data/constants.ts index 17a217ada..2eadce9ae 100644 --- a/data/constants.ts +++ b/data/constants.ts @@ -1,2 +1,2 @@ export const REPO_URL = 'ajnart/homarr'; -export const CURRENT_VERSION = 'v0.7.0'; +export const CURRENT_VERSION = 'v0.7.1'; diff --git a/package.json b/package.json index 622116f36..1d407ac5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homarr", - "version": "0.7.0", + "version": "0.7.1", "description": "Homarr - A homepage for your server.", "repository": { "type": "git", diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index be6034f6b..f9229d220 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -12,6 +12,10 @@ import { Title, Anchor, Text, + Tabs, + MultiSelect, + ScrollArea, + Switch, } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useEffect, useState } from 'react'; @@ -19,7 +23,7 @@ import { IconApps as Apps } from '@tabler/icons'; import { v4 as uuidv4 } from 'uuid'; import { useDebouncedValue } from '@mantine/hooks'; import { useConfig } from '../../tools/state'; -import { ServiceTypeList } from '../../tools/types'; +import { ServiceTypeList, StatusCodes } from '../../tools/types'; export function AddItemShelfButton(props: any) { const [opened, setOpened] = useState(false); @@ -113,6 +117,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & username: props.username ?? (undefined as unknown as string), password: props.password ?? (undefined as unknown as string), openedUrl: props.openedUrl ?? (undefined as unknown as string), + status: props.status ?? ['200'], + newTab: props.newTab ?? true, }, validate: { apiKey: () => null, @@ -133,6 +139,12 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & } return null; }, + status: (value: string[]) => { + if (!value.length) { + return 'Please select a status code'; + } + return null; + }, }, }); @@ -167,6 +179,12 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
{ + if (JSON.stringify(form.values.status) === JSON.stringify(['200'])) { + form.values.status = undefined; + } + if (form.values.newTab === true) { + form.values.newTab = undefined; + } // If service already exists, update it. if (config.services && config.services.find((s) => s.id === form.values.id)) { setConfig({ @@ -191,131 +209,167 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & form.reset(); })} > - - + + + + + - - - - { - e.preventDefault(); - }} - getCreateLabel={(query) => `+ Create "${query}"`} - onCreate={(query) => {}} - {...form.getInputProps('category')} - /> - - {(form.values.type === 'Sonarr' || - form.values.type === 'Radarr' || - form.values.type === 'Lidarr' || - form.values.type === 'Readarr') && ( - <> - + + + { + e.preventDefault(); + }} + getCreateLabel={(query) => `+ Create "${query}"`} + onCreate={(query) => {}} + {...form.getInputProps('category')} + /> + + {(form.values.type === 'Sonarr' || + form.values.type === 'Radarr' || + form.values.type === 'Lidarr' || + form.values.type === 'Readarr') && ( + <> + { + form.setFieldValue('apiKey', event.currentTarget.value); + }} + error={form.errors.apiKey && 'Invalid API key'} + /> + + Tip: Get your API key{' '} + + here. + + + + )} + {form.values.type === 'qBittorrent' && ( + <> + { + form.setFieldValue('username', event.currentTarget.value); + }} + error={form.errors.username && 'Invalid username'} + /> + { + form.setFieldValue('password', event.currentTarget.value); + }} + error={form.errors.password && 'Invalid password'} + /> + + )} + {(form.values.type === 'Deluge' || + form.values.type === 'Transmission' || + form.values.type === 'qBittorrent') && ( + <> + { + form.setFieldValue('username', event.currentTarget.value); + }} + error={form.errors.username && 'Invalid username'} + /> + { + form.setFieldValue('password', event.currentTarget.value); + }} + error={form.errors.password && 'Invalid password'} + /> + + )} + + + + + + { - form.setFieldValue('apiKey', event.currentTarget.value); - }} - error={form.errors.apiKey && 'Invalid API key'} + label="HTTP Status Codes" + data={StatusCodes} + placeholder="Select valid status codes" + clearButtonLabel="Clear selection" + nothingFound="Nothing found" + defaultValue={['200']} + clearable + searchable + {...form.getInputProps('status')} /> - - Tip: Get your API key{' '} - - here. - - - - )} - {form.values.type === 'qBittorrent' && ( - <> - { - form.setFieldValue('username', event.currentTarget.value); - }} - error={form.errors.username && 'Invalid username'} + - { - form.setFieldValue('password', event.currentTarget.value); - }} - error={form.errors.password && 'Invalid password'} - /> - - )} - {(form.values.type === 'Deluge' || form.values.type === 'Transmission') && ( - <> - { - form.setFieldValue('password', event.currentTarget.value); - }} - error={form.errors.password && 'Invalid password'} - /> - - )} - - + + + diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index 97b028997..05c0d1ada 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -44,11 +44,16 @@ const AppShelf = (props: any) => { const { colorScheme } = useMantineColorScheme(); const sensors = useSensors( - useSensor(TouchSensor, {}), + useSensor(TouchSensor, { + activationConstraint: { + delay: 500, + tolerance: 5, + }, + }), useSensor(MouseSensor, { // Require the mouse to move by 10 pixels before activating activationConstraint: { - delay: 250, + delay: 500, tolerance: 5, }, }) @@ -101,7 +106,14 @@ const AppShelf = (props: any) => { {filtered.map((service) => ( - + ))} diff --git a/src/components/AppShelf/AppShelfItem.tsx b/src/components/AppShelf/AppShelfItem.tsx index 8ad389a19..109b873c2 100644 --- a/src/components/AppShelf/AppShelfItem.tsx +++ b/src/components/AppShelf/AppShelfItem.tsx @@ -83,8 +83,8 @@ export function AppShelfItem(props: any) { > @@ -127,13 +127,14 @@ export function AppShelfItem(props: any) { src={service.icon} fit="contain" onClick={() => { - if (service.openedUrl) window.open(service.openedUrl, '_blank'); - else window.open(service.url); + if (service.openedUrl) { + window.open(service.openedUrl, service.newTab === false ? '_top' : '_blank'); + } else window.open(service.url, service.newTab === false ? '_top' : '_blank'); }} /> - + diff --git a/src/components/AppShelf/AppShelfMenu.tsx b/src/components/AppShelf/AppShelfMenu.tsx index 824b9ae7a..5aada187a 100644 --- a/src/components/AppShelf/AppShelfMenu.tsx +++ b/src/components/AppShelf/AppShelfMenu.tsx @@ -22,16 +22,7 @@ export default function AppShelfMenu(props: any) { > diff --git a/src/components/Settings/AdvancedSettings.tsx b/src/components/Settings/AdvancedSettings.tsx index b00a11933..ad4517457 100644 --- a/src/components/Settings/AdvancedSettings.tsx +++ b/src/components/Settings/AdvancedSettings.tsx @@ -3,6 +3,7 @@ import { useForm } from '@mantine/form'; import { useConfig } from '../../tools/state'; import { ColorSelector } from './ColorSelector'; import { OpacitySelector } from './OpacitySelector'; +import { AppCardWidthSelector } from './AppCardWidthSelector'; import { ShadeSelector } from './ShadeSelector'; export default function TitleChanger() { @@ -58,6 +59,7 @@ export default function TitleChanger() { + ); } diff --git a/src/components/Settings/AppCardWidthSelector.tsx b/src/components/Settings/AppCardWidthSelector.tsx new file mode 100644 index 000000000..945778e67 --- /dev/null +++ b/src/components/Settings/AppCardWidthSelector.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Group, Text, Slider } from '@mantine/core'; +import { useConfig } from '../../tools/state'; + +export function AppCardWidthSelector() { + const { config, setConfig } = useConfig(); + + const setappCardWidth = (appCardWidth: number) => { + setConfig({ + ...config, + settings: { + ...config.settings, + appCardWidth, + }, + }); + }; + + return ( + + App Width + setappCardWidth(value)} + /> + + ); +} diff --git a/src/components/modules/calendar/CalendarModule.tsx b/src/components/modules/calendar/CalendarModule.tsx index a0b6574d1..4665f4480 100644 --- a/src/components/modules/calendar/CalendarModule.tsx +++ b/src/components/modules/calendar/CalendarModule.tsx @@ -29,6 +29,12 @@ export const CalendarModule: IModule = { 'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.', icon: CalendarIcon, component: CalendarComponent, + options: { + sundaystart: { + name: 'Start the week on Sunday', + value: false, + }, + }, }; export default function CalendarComponent(props: any) { @@ -62,41 +68,49 @@ export default function CalendarComponent(props: any) { useEffect(() => { // Create each Sonarr service and get the medias - const currentSonarrMedias: any[] = [...sonarrMedias]; + const currentSonarrMedias: any[] = []; Promise.all( sonarrServices.map((service) => getMedias(service, 'sonarr').then((res) => { currentSonarrMedias.push(...res.data); + }).catch(() => { + currentSonarrMedias.push([]); }) ) ).then(() => { setSonarrMedias(currentSonarrMedias); }); - const currentRadarrMedias: any[] = [...radarrMedias]; + const currentRadarrMedias: any[] = []; Promise.all( radarrServices.map((service) => getMedias(service, 'radarr').then((res) => { currentRadarrMedias.push(...res.data); + }).catch(() => { + currentRadarrMedias.push([]); }) ) ).then(() => { setRadarrMedias(currentRadarrMedias); }); - const currentLidarrMedias: any[] = [...lidarrMedias]; + const currentLidarrMedias: any[] = []; Promise.all( lidarrServices.map((service) => getMedias(service, 'lidarr').then((res) => { currentLidarrMedias.push(...res.data); + }).catch(() => { + currentLidarrMedias.push([]); }) ) ).then(() => { setLidarrMedias(currentLidarrMedias); }); - const currentReadarrMedias: any[] = [...readarrMedias]; + const currentReadarrMedias: any[] = []; Promise.all( readarrServices.map((service) => getMedias(service, 'readarr').then((res) => { currentReadarrMedias.push(...res.data); + }).catch(() => { + currentReadarrMedias.push([]); }) ) ).then(() => { @@ -104,8 +118,11 @@ export default function CalendarComponent(props: any) { }); }, [config.services]); + const weekStartsAtSunday = + (config?.modules?.[CalendarModule.title]?.options?.sundaystart?.value as boolean) ?? false; return ( {}} dayStyle={(date) => date.getDay() === today.getDay() && date.getDate() === today.getDate() @@ -115,6 +132,13 @@ export default function CalendarComponent(props: any) { } : {} } + styles={{ + calendarHeader: { + marginRight: 15, + marginLeft: 15, + }, + }} + allowLevelChange={false} dayClassName={(date, modifiers) => cx({ [classes.weekend]: modifiers.weekend })} renderDay={(renderdate) => ( diff --git a/src/components/modules/ping/PingModule.story.tsx b/src/components/modules/ping/PingModule.story.tsx index 52a832da5..c89b882e1 100644 --- a/src/components/modules/ping/PingModule.story.tsx +++ b/src/components/modules/ping/PingModule.story.tsx @@ -11,6 +11,8 @@ const service: serviceItem = { name: 'YouTube', icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png', url: 'https://youtube.com/', + status: ['200'], + newTab: false, }; export const Default = (args: any) => ; diff --git a/src/components/modules/ping/PingModule.tsx b/src/components/modules/ping/PingModule.tsx index c7586f538..b77d8c2fd 100644 --- a/src/components/modules/ping/PingModule.tsx +++ b/src/components/modules/ping/PingModule.tsx @@ -1,5 +1,5 @@ import { Indicator, Tooltip } from '@mantine/core'; -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { motion } from 'framer-motion'; import { useEffect, useState } from 'react'; import { IconPlug as Plug } from '@tabler/icons'; @@ -19,18 +19,37 @@ export default function PingComponent(props: any) { const { url }: { url: string } = props; const [isOnline, setOnline] = useState('loading'); + const [response, setResponse] = useState(500); const exists = config.modules?.[PingModule.title]?.enabled ?? false; + + function statusCheck(response: AxiosResponse) { + const { status }: {status: string[]} = props; + //Default Status + let acceptableStatus = ['200']; + if (status !== undefined && status.length) { + acceptableStatus = status; + } + // Checks if reported status is in acceptable status array + if (acceptableStatus.indexOf((response.status).toString()) >= 0) { + setOnline('online'); + setResponse(response.status); + } else { + setOnline('down'); + setResponse(response.status) + } + } + useEffect(() => { if (!exists) { return; } axios .get('/api/modules/ping', { params: { url } }) - .then(() => { - setOnline('online'); + .then((response) => { + statusCheck(response); }) - .catch(() => { - setOnline('down'); + .catch((error) => { + statusCheck(error.response); }); }, [config.modules?.[PingModule.title]?.enabled]); if (!exists) { @@ -40,7 +59,7 @@ export default function PingComponent(props: any) { { - res.status(200).json(response.data); + res.status(response.status).json(response.statusText); }) .catch((error) => { - res.status(500).json(error); + if (error.response) { + res.status(error.response.status).json(error.response.statusText); + } else { + res.status(500).json('Server Error'); + } }); // // Make a request to the URL // const response = await axios.get(url); diff --git a/src/tools/types.ts b/src/tools/types.ts index 92d5345ac..6ed888930 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -12,6 +12,7 @@ export interface Settings { background?: string; appOpacity?: number; widgetPosition?: string; + appCardWidth?: number; } export interface Config { @@ -31,6 +32,32 @@ interface ConfigModule { }; } +export const StatusCodes = [ + {value: '200', label: '200 - OK', group:'Sucessful responses'}, + {value: '204', label: '204 - No Content', group:'Sucessful responses'}, + {value: '301', label: '301 - Moved Permanently', group:'Redirection responses'}, + {value: '302', label: '302 - Found / Moved Temporarily', group:'Redirection responses'}, + {value: '304', label: '304 - Not Modified', group:'Redirection responses'}, + {value: '307', label: '307 - Temporary Redirect', group:'Redirection responses'}, + {value: '308', label: '308 - Permanent Redirect', group:'Redirection responses'}, + {value: '400', label: '400 - Bad Request', group:'Client error responses'}, + {value: '401', label: '401 - Unauthorized', group:'Client error responses'}, + {value: '403', label: '403 - Forbidden', group:'Client error responses'}, + {value: '404', label: '404 - Not Found', group:'Client error responses'}, + {value: '408', label: '408 - Request Timeout', group:'Client error responses'}, + {value: '410', label: '410 - Gone', group:'Client error responses'}, + {value: '429', label: '429 - Too Many Requests', group:'Client error responses'}, + {value: '500', label: '500 - Internal Server Error', group:'Server error responses'}, + {value: '502', label: '502 - Bad Gateway', group:'Server error responses'}, + {value: '503', label: '503 - Service Unavailable', group:'Server error responses'}, + {value: '054', label: '504 - Gateway Timeout Error', group:'Server error responses'}, + ]; + +export const Targets = [ + {value: '_blank', label: 'New Tab'}, + {value: '_top', label: 'Same Window'} +] + export const ServiceTypeList = [ 'Other', 'Emby', @@ -66,4 +93,6 @@ export interface serviceItem { password?: string; username?: string; openedUrl?: string; + newTab?: boolean; + status?: string[]; }