diff --git a/data/constants.ts b/data/constants.ts index 8ca22cd0b..2dfb47db2 100644 --- a/data/constants.ts +++ b/data/constants.ts @@ -1,2 +1,3 @@ export const REPO_URL = 'ajnart/homarr'; -export const CURRENT_VERSION = 'v0.10.7'; +export const CURRENT_VERSION = 'v0.11'; +export const ICON_PICKER_SLICE_LIMIT = 36; diff --git a/package.json b/package.json index 0cc6099e9..dec99fed3 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,16 @@ "@dnd-kit/utilities": "^3.2.0", "@emotion/react": "^11.10.5", "@emotion/server": "^11.10.0", - "@mantine/carousel": "^5.9.0", - "@mantine/core": "^5.9.0", - "@mantine/dates": "^5.9.0", - "@mantine/dropzone": "^5.9.0", - "@mantine/form": "^5.9.0", - "@mantine/hooks": "^5.9.0", - "@mantine/modals": "^5.9.0", - "@mantine/next": "^5.9.0", - "@mantine/notifications": "^5.9.0", - "@mantine/prism": "^5.9.0", + "@mantine/carousel": "^5.9.3", + "@mantine/core": "^5.9.3", + "@mantine/dates": "^5.9.3", + "@mantine/dropzone": "^5.9.3", + "@mantine/form": "^5.9.3", + "@mantine/hooks": "^5.9.3", + "@mantine/modals": "^5.9.3", + "@mantine/next": "^5.9.3", + "@mantine/notifications": "^5.9.3", + "@mantine/prism": "^5.9.3", "@nivo/core": "^0.79.0", "@nivo/line": "^0.79.1", "@tabler/icons": "^1.106.0", diff --git a/public/locales/en/layout/header/actions/toggle-edit-mode.json b/public/locales/en/layout/header/actions/toggle-edit-mode.json new file mode 100644 index 000000000..3ae95e24f --- /dev/null +++ b/public/locales/en/layout/header/actions/toggle-edit-mode.json @@ -0,0 +1,11 @@ +{ + "tooltip": "The edit mode enables you to configure your dashboard", + "button": { + "disabled": "Enter Edit Mode", + "enabled": "Exit and Save" + }, + "popover": { + "title": "Edit mode is enabled", + "text": "You can adjust and configure your apps now. Changes are not saved until you exit edit mode" + } +} \ No newline at end of file diff --git a/src/components/About/AboutModal.tsx b/src/components/About/AboutModal.tsx index bd51bddc0..add258043 100644 --- a/src/components/About/AboutModal.tsx +++ b/src/components/About/AboutModal.tsx @@ -1,4 +1,3 @@ -import Image from 'next/image'; import { ActionIcon, Badge, @@ -18,7 +17,9 @@ import { IconVocabulary, IconWorldWww, } from '@tabler/icons'; +import { InitOptions } from 'i18next'; import { i18n } from 'next-i18next'; +import Image from 'next/image'; import { ReactNode } from 'react'; import { CURRENT_VERSION } from '../../../data/constants'; import { usePrimaryGradient } from '../layout/useGradient'; @@ -118,13 +119,44 @@ interface InformationTableItem { content: ReactNode; } +interface ExtendedInitOptions extends InitOptions { + locales: string[]; +} + const useInformationTableItems = (): InformationTableItem[] => { const colorGradiant = usePrimaryGradient(); - const usedI18nNamespaces = i18n?.reportNamespaces?.getUsedNamespaces(); - const configuredi18nLocales: string[] = i18n?.options.locales; + let items: InformationTableItem[] = []; - return [ + if (i18n !== null) { + const usedI18nNamespaces = i18n.reportNamespaces.getUsedNamespaces(); + const initOptions = i18n.options as ExtendedInitOptions; + + items = [ + ...items, + { + icon: , + label: 'Loaded I18n translation namespaces', + content: ( + + {usedI18nNamespaces.length} + + ), + }, + { + icon: , + label: 'Configured I18n locales', + content: ( + + {initOptions.locales.length} + + ), + }, + ]; + } + + items = [ + ...items, { icon: , label: 'Homarr version', @@ -134,25 +166,9 @@ const useInformationTableItems = (): InformationTableItem[] => { ), }, - { - icon: , - label: 'Loaded I18n translation namespaces', - content: ( - - {usedI18nNamespaces?.length ?? 'loading'} - - ), - }, - { - icon: , - label: 'Configured I18n locales', - content: ( - - {configuredi18nLocales?.length ?? 'loading'} - - ), - }, ]; + + return items; }; const useStyles = createStyles(() => ({ diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx deleted file mode 100644 index 3b89d1c85..000000000 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ /dev/null @@ -1,460 +0,0 @@ -import { - ActionIcon, - Anchor, - Button, - Center, - Group, - Image, - LoadingOverlay, - Modal, - PasswordInput, - Select, - Space, - Stack, - Switch, - Tabs, - TextInput, - Title, - Tooltip, -} from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useDebouncedValue } from '@mantine/hooks'; -import { IconApps } from '@tabler/icons'; -import { useTranslation } from 'next-i18next'; -import { useEffect, useState } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import { useConfig } from '../../tools/state'; -import { tryMatchPort, ServiceTypeList, Config } from '../../tools/types'; -import apiKeyPaths from './apiKeyPaths.json'; -import Tip from '../layout/Tip'; - -export function AddItemShelfButton(props: any) { - const { config, setConfig } = useConfig(); - const [opened, setOpened] = useState(false); - const { t } = useTranslation('layout/add-service-app-shelf'); - return ( - <> - {t('modal.title')}} - opened={props.opened || opened} - onClose={() => setOpened(false)} - > - - - - setOpened(true)} - > - - - - > - ); -} - -function MatchIcon(name: string | undefined, form: any) { - if (name === undefined || name === '') return null; - fetch( - `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name - .replace(/\s+/g, '-') - .toLowerCase() - .replace(/^dash\.$/, 'dashdot')}.png` - ).then((res) => { - if (res.ok) { - form.setFieldValue('icon', res.url); - } - }); - - return false; -} - -function MatchService(name: string, form: any) { - const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase()); - if (service) { - form.setFieldValue('type', service); - } -} - -const DEFAULT_ICON = '/imgs/favicon/favicon.png'; - -interface AddAppShelfItemFormProps { - setOpened: (b: boolean) => void; - config: Config; - setConfig: (config: Config) => void; - // Any other props you want to pass to the form - [key: string]: any; -} - -export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) { - const { setOpened, config, setConfig } = props; - // Only get config and setConfig from useCOnfig if they are not present in props - const [isLoading, setLoading] = useState(false); - const { t } = useTranslation('layout/add-service-app-shelf'); - - // Extract all the categories from the services in config - const InitialCategories = config.services.reduce((acc, cur) => { - if (cur.category && !acc.includes(cur.category)) { - acc.push(cur.category); - } - return acc; - }, [] as string[]); - const [categories, setCategories] = useState(InitialCategories); - - const form = useForm({ - initialValues: { - id: props.id ?? uuidv4(), - type: props.type ?? 'Other', - category: props.category ?? null, - name: props.name ?? '', - icon: props.icon ?? DEFAULT_ICON, - url: props.url ?? '', - apiKey: props.apiKey ?? undefined, - username: props.username ?? undefined, - password: props.password ?? undefined, - openedUrl: props.openedUrl ?? undefined, - ping: props.ping ?? true, - newTab: props.newTab ?? true, - }, - validate: { - apiKey: () => null, - // Validate icon with a regex - icon: (value: string) => - // Disable matching to allow any values - null, - // Validate url with a regex http/https - url: (value: string) => { - try { - const _isValid = new URL(value); - } catch (e) { - return t('modal.form.validation.invalidUrl'); - } - return null; - }, - }, - }); - - const [debounced, cancel] = useDebouncedValue(form.values.name, 250); - useEffect(() => { - if ( - form.values.name !== debounced || - form.values.icon !== DEFAULT_ICON || - form.values.type !== 'Other' - ) { - return; - } - MatchIcon(form.values.name, form); - MatchService(form.values.name, form); - tryMatchPort(form.values.name, form); - }, [debounced]); - - // Try to set const hostname to new URL(form.values.url).hostname) - // If it fails, set it to the form.values.url - let hostname = form.values.url; - try { - hostname = new URL(form.values.url).origin; - } catch (e) { - // Do nothing - } - - return ( - <> - - - - { - const newForm = { ...form.values }; - if (newForm.newTab === true) newForm.newTab = undefined; - if (newForm.openedUrl === '') newForm.openedUrl = undefined; - if (newForm.category === null) newForm.category = undefined; - if (newForm.ping === true) newForm.ping = undefined; - // If service already exists, update it. - if (config.services && config.services.find((s) => s.id === newForm.id)) { - setConfig({ - ...config, - // replace the found item by matching ID - services: config.services.map((s) => { - if (s.id === newForm.id) { - return { - ...newForm, - }; - } - return s; - }), - }); - } else { - setConfig({ - ...config, - services: [...config.services, newForm], - }); - } - setOpened(false); - form.reset(); - })} - > - - - {t('modal.tabs.options.title')} - {t('modal.tabs.advancedOptions.title')} - - - - - - - - - - { - const item = { value: query, label: query }; - setCategories([...InitialCategories, query]); - return item; - }} - getCreateLabel={(query) => - t('modal.tabs.options.form.category.createLabel', { - query, - }) - } - {...form.getInputProps('category')} - /> - - {(form.values.type === 'Sonarr' || - form.values.type === 'Radarr' || - form.values.type === 'Lidarr' || - form.values.type === 'Overseerr' || - form.values.type === 'Jellyseerr' || - form.values.type === 'Readarr' || - form.values.type === 'Sabnzbd') && ( - <> - { - form.setFieldValue('apiKey', event.currentTarget.value); - }} - error={ - form.errors.apiKey && - t('modal.tabs.options.form.integrations.apiKey.validation.noKey') - } - /> - - {t('modal.tabs.options.form.integrations.apiKey.tip.text')}{' '} - - {t('modal.tabs.options.form.integrations.apiKey.tip.link')} - - - > - )} - {form.values.type === 'qBittorrent' && ( - <> - { - form.setFieldValue('username', event.currentTarget.value); - }} - error={ - form.errors.username && - t( - 'modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername' - ) - } - /> - { - form.setFieldValue('password', event.currentTarget.value); - }} - error={ - form.errors.password && - t( - 'modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword' - ) - } - /> - > - )} - {form.values.type === 'Deluge' && ( - <> - { - form.setFieldValue('password', event.currentTarget.value); - }} - error={ - form.errors.password && - t( - 'modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword' - ) - } - /> - > - )} - {form.values.type === 'Transmission' && ( - <> - { - form.setFieldValue('username', event.currentTarget.value); - }} - error={ - form.errors.username && - t( - 'modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername' - ) - } - /> - { - form.setFieldValue('password', event.currentTarget.value); - }} - error={ - form.errors.password && - t( - 'modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword' - ) - } - /> - > - )} - {form.values.type === 'NZBGet' && ( - <> - { - form.setFieldValue('username', event.currentTarget.value); - }} - error={ - form.errors.username && - t( - 'modal.tabs.options.form.integrations.nzbget.username.validation.invalidUsername' - ) - } - /> - { - form.setFieldValue('password', event.currentTarget.value); - }} - error={ - form.errors.password && - t( - 'modal.tabs.options.form.integrations.nzbget.password.validation.invalidPassword' - ) - } - /> - > - )} - - - - - - - - - - - - - {props.message ?? t('modal.tabs.advancedOptions.form.buttons.submit.content')} - - - - > - ); -} diff --git a/src/components/AppShelf/AppShelfItem.tsx b/src/components/AppShelf/AppShelfItem.tsx index bbc51122c..cbd9744bc 100644 --- a/src/components/AppShelf/AppShelfItem.tsx +++ b/src/components/AppShelf/AppShelfItem.tsx @@ -15,7 +15,7 @@ import { useState } from 'react'; import PingComponent from '../../modules/ping/PingModule'; import { useConfig } from '../../tools/state'; import { serviceItem } from '../../tools/types'; -import AppShelfMenu from './AppShelfMenu'; +import { TileMenu } from '../Dashboard/Menu/TileMenu'; const useStyles = createStyles((theme) => ({ item: { @@ -104,7 +104,7 @@ export function AppShelfItem(props: any) { opacity: hovering ? 1 : 0, }} > - + { /* TODO: Remove this component */ } diff --git a/src/components/AppShelf/AppShelfMenu.tsx b/src/components/AppShelf/AppShelfMenu.tsx deleted file mode 100644 index 402867ac0..000000000 --- a/src/components/AppShelf/AppShelfMenu.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ActionIcon, Menu, Modal, Text } from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; -import { useState } from 'react'; -import { IconCheck as Check, IconEdit as Edit, IconMenu, IconTrash as Trash } from '@tabler/icons'; -import { useTranslation } from 'next-i18next'; -import { useConfig } from '../../tools/state'; -import { serviceItem } from '../../tools/types'; -import { AddAppShelfItemForm } from './AddAppShelfItem'; -import { useColorTheme } from '../../tools/color'; - -export default function AppShelfMenu(props: any) { - const { service }: { service: serviceItem } = props; - const { config, setConfig } = useConfig(); - const { secondaryColor } = useColorTheme(); - const { t } = useTranslation('layout/app-shelf-menu'); - const [opened, setOpened] = useState(false); - return ( - <> - setOpened(false)} - title={t('modal.title')} - > - - - - - - - - - - {t('menu.labels.settings')} - } onClick={() => setOpened(true)}> - {t('menu.actions.edit')} - - {t('menu.labels.dangerZone')} - { - setConfig({ - ...config, - services: config.services.filter((s) => s.id !== service.id), - }); - showNotification({ - autoClose: 5000, - title: ( - - Service {service.name} removed successfully! - - ), - color: 'green', - icon: , - message: undefined, - }); - }} - icon={} - > - {t('menu.actions.delete')} - - - - > - ); -} diff --git a/src/components/Dashboard/Menu/TileMenu.tsx b/src/components/Dashboard/Menu/TileMenu.tsx new file mode 100644 index 000000000..22c0c1de6 --- /dev/null +++ b/src/components/Dashboard/Menu/TileMenu.tsx @@ -0,0 +1,76 @@ +import { ActionIcon, Menu, Text } from '@mantine/core'; +import { openContextModal } from '@mantine/modals'; +import { showNotification } from '@mantine/notifications'; +import { IconCheck, IconEdit, IconMenu, IconTrash } from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; +import { useConfigContext } from '../../../config/provider'; +import { useConfigStore } from '../../../config/store'; +import { useColorTheme } from '../../../tools/color'; +import { ServiceType } from '../../../types/service'; + +interface TileMenuProps { + service: ServiceType; +} + +export const TileMenu = ({ service }: TileMenuProps) => { + const { secondaryColor } = useColorTheme(); + const { t } = useTranslation('layout/app-shelf-menu'); + const updateConfig = useConfigStore((x) => x.updateConfig); + const { name: configName } = useConfigContext(); + + return ( + + + + + + + + {t('menu.labels.settings')} + } + onClick={() => + openContextModal({ + modal: 'changeTilePosition', + innerProps: { + type: 'service', + tile: service, + }, + }) + } + > + {t('menu.actions.edit')} + + {t('menu.labels.dangerZone')} + { + if (!configName) { + return; + } + updateConfig(configName, (previousConfig) => ({ + ...previousConfig, + services: previousConfig.services.filter((x) => x.id !== service.id), + })).then(() => { + showNotification({ + autoClose: 5000, + title: ( + + Service {service.name} removed successfully! + + ), + color: 'green', + icon: , + message: undefined, + }); + }); + }} + icon={} + > + {t('menu.actions.delete')} + + + + ); +}; diff --git a/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx b/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx new file mode 100644 index 000000000..7c6aec0c4 --- /dev/null +++ b/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx @@ -0,0 +1,94 @@ +import { Button, Flex, Grid, NumberInput } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { closeModal, ContextModalProps } from '@mantine/modals'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; +import { ServiceType } from '../../../../types/service'; +import { TileBaseType } from '../../../../types/tile'; + +export const ChangePositionModal = ({ + context, + id, + innerProps, +}: ContextModalProps<{ type: 'service' | 'type'; tile: TileBaseType }>) => { + const updateConfig = useConfigStore((x) => x.updateConfig); + const { name: configName } = useConfigContext(); + + const form = useForm({ + initialValues: { + tile: innerProps.tile, + }, + validateInputOnChange: true, + validateInputOnBlur: true, + }); + + const onSubmit = () => { + if (!configName) { + return; + } + + const tileAsService = form.values.tile as ServiceType; + + updateConfig(configName, (previous) => ({ + ...previous, + services: [...previous.services.filter((x) => x.id === tileAsService.id), tileAsService], + })); + + closeModal(id); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + closeModal(id)} variant="light" color="gray"> + Cancel + + Change Position + + + ); +}; diff --git a/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx b/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx index ba2f86161..576dff849 100644 --- a/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx +++ b/src/components/Dashboard/Modals/EditService/EditServiceModal.tsx @@ -1,24 +1,30 @@ -import Image from 'next/image'; -import { Button, createStyles, Group, Stack, Tabs, Text } from '@mantine/core'; +import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core'; import { useForm } from '@mantine/form'; -import { closeModal, ContextModalProps } from '@mantine/modals'; +import { ContextModalProps } from '@mantine/modals'; +import { hideNotification, showNotification } from '@mantine/notifications'; import { IconAccessPoint, IconAdjustments, + IconAlertTriangle, IconBrush, IconClick, - IconDeviceFloppy, - IconDoorExit, IconPlug, } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; -import { hideNotification, showNotification } from '@mantine/notifications'; +import Image from 'next/image'; +import { useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; import { ServiceType } from '../../../../types/service'; import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab'; import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab'; import { GeneralTab } from './Tabs/GeneralTab/GeneralTab'; import { IntegrationTab } from './Tabs/IntegrationTab/IntegrationTab'; import { NetworkTab } from './Tabs/NetworkTab/NetworkTab'; +import { EditServiceModalTab } from './Tabs/type'; + +const serviceUrlRegex = + '(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 = ({ context, @@ -27,16 +33,66 @@ export const EditServiceModal = ({ }: ContextModalProps<{ service: ServiceType }>) => { const { t } = useTranslation(); const { classes } = useStyles(); + const { name: configName, config } = useConfigContext(); + const updateConfig = useConfigStore((store) => store.updateConfig); const form = useForm({ initialValues: innerProps.service, + validate: { + name: (name) => (!name ? 'Name is required' : null), + url: (url) => { + if (!url) { + return 'Url is required'; + } + + if (!url.match(serviceUrlRegex)) { + return 'Value is not a valid url'; + } + + return null; + }, + appearance: { + iconUrl: (url: string) => { + if (url.length < 1) { + return 'This field is required'; + } + + return null; + }, + }, + behaviour: { + onClickUrl: (url: string) => { + if (url === undefined || url.length < 1) { + return null; + } + + if (!url.match(serviceUrlRegex)) { + return 'Uri override is not a valid uri'; + } + + return null; + }, + }, + }, + validateInputOnChange: true, }); const onSubmit = (values: ServiceType) => { - console.log('form submitted'); - console.log(values); + if (!configName) { + return; + } + + updateConfig(configName, (previousConfig) => ({ + ...previousConfig, + services: [...previousConfig.services.filter((x) => x.id !== form.values.id), form.values], + })); + + // also close the parent modal + context.closeAll(); }; + const [activeTab, setActiveTab] = useState('general'); + const tryCloseModal = () => { if (form.isDirty()) { showNotification({ @@ -63,8 +119,27 @@ export const EditServiceModal = ({ context.closeModal(id); }; + const validationErrors = Object.keys(form.errors); + + const ValidationErrorIndicator = ({ keys }: { keys: string[] }) => { + const relevantErrors = validationErrors.filter((x) => keys.includes(x)); + + return ( + + + + ); + }; + return ( <> + {configName === undefined || + (config === undefined && ( + + There was an unexpected problem loading the configuration. Functionality might be + restricted. Please report this incident. + + ))} {form.values.appearance.iconUrl ? ( // disabled because image target is too dynamic for next image cache @@ -84,27 +159,52 @@ export const EditServiceModal = ({ {form.values.name ?? 'New Service'} + - + setActiveTab(tab as EditServiceModalTab)} + defaultValue="general" + > - }> + } + icon={} + value="general" + > General - }> + } + icon={} + value="behaviour" + > Behaviour - }> + } + icon={} + value="network" + > Network - }> + } + icon={} + value="appearance" + > Appearance - }> + } + icon={} + value="integration" + > Integration - + setActiveTab(targetTab)} /> @@ -112,16 +212,10 @@ export const EditServiceModal = ({ - } - px={50} - variant="light" - color="gray" - onClick={tryCloseModal} - > + Cancel - } px={50}> + Save diff --git a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx index 1066a28df..17dc43874 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/AppereanceTab.tsx @@ -1,8 +1,9 @@ -import { Tabs, TextInput, createStyles } from '@mantine/core'; +import Image from 'next/image'; +import { createStyles, Flex, Tabs, TextInput } from '@mantine/core'; import { UseFormReturnType } from '@mantine/form'; -import { IconPhoto } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { ServiceType } from '../../../../../../types/service'; +import { IconSelector } from './IconSelector/IconSelector'; interface AppearanceTabProps { form: UseFormReturnType ServiceType>; @@ -12,32 +13,56 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => { const { t } = useTranslation(''); const { classes } = useStyles(); - const Image = () => { - if (form.values.appearance.iconUrl !== undefined) { + const PreviewImage = () => { + if (form.values.appearance.iconUrl !== undefined && form.values.appearance.iconUrl.length > 0) { // disabled due to too many dynamic targets for next image cache // eslint-disable-next-line @next/next/no-img-element - return ; + return ; } - return ; + return ( + + ); }; return ( - } - label="Service Icon" - variant="default" - defaultValue={form.values.appearance.iconUrl} - {...form.getInputProps('appearance.iconUrl')} - withAsterisk - required - /> + + } + label="Service Icon" + description="Logo of your service displayed in your dashboard. Must return a body content containg an image" + variant="default" + withAsterisk + required + {...form.getInputProps('appearance.iconUrl')} + /> + + form.setValues({ + appearance: { + iconUrl: item.url, + }, + }) + } + /> + ); }; const useStyles = createStyles(() => ({ + textInput: { + flexGrow: 1, + }, iconImage: { objectFit: 'contain', width: 20, diff --git a/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx new file mode 100644 index 000000000..b82a2a49d --- /dev/null +++ b/src/components/Dashboard/Modals/EditService/Tabs/AppereanceTab/IconSelector/IconSelector.tsx @@ -0,0 +1,117 @@ +/* eslint-disable @next/next/no-img-element */ +import { + ActionIcon, + Button, + createStyles, + Divider, + Flex, + Loader, + Popover, + ScrollArea, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; +import { IconSearch, IconX } from '@tabler/icons'; +import { useState } from 'react'; +import { ICON_PICKER_SLICE_LIMIT } from '../../../../../../../../data/constants'; +import { useRepositoryIconsQuery } from '../../../../../../../tools/hooks/useRepositoryIconsQuery'; +import { IconSelectorItem } from '../../../../../../../types/iconSelector/iconSelectorItem'; +import { WalkxcodeRepositoryIcon } from '../../../../../../../types/iconSelector/repositories/walkxcodeIconRepository'; + +interface IconSelectorProps { + onChange: (icon: IconSelectorItem) => void; +} + +export const IconSelector = ({ onChange }: IconSelectorProps) => { + const { data, isLoading } = useRepositoryIconsQuery({ + url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png', + converter: (item) => ({ + url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`, + }), + }); + + const [searchTerm, setSearchTerm] = useState(''); + const { classes } = useStyles(); + + if (isLoading || !data) { + return ; + } + + const replaceCharacters = (value: string) => + value.toLowerCase().replaceAll(' ', '').replaceAll('-', ''); + + const filteredItems = searchTerm + ? data.filter((x) => replaceCharacters(x.url).includes(replaceCharacters(searchTerm))) + : data; + const slicedFilteredItems = filteredItems.slice(0, ICON_PICKER_SLICE_LIMIT); + const isTruncated = + slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length; + + return ( + + + } + > + Icon Picker + + + + + setSearchTerm(event.currentTarget.value)} + placeholder="Search for icons..." + variant="filled" + rightSection={ + setSearchTerm('')}> + + + } + /> + + + + {slicedFilteredItems.map((item) => ( + onChange(item)} size={40} p={3}> + + + ))} + + + {isTruncated && ( + + + + Search is limited to {ICON_PICKER_SLICE_LIMIT} icons + + + To keep things snappy and fast, the search is limited to {ICON_PICKER_SLICE_LIMIT}{' '} + icons. Use the search box to find more icons. + + + )} + + + + + ); +}; + +const useStyles = createStyles(() => ({ + flameIcon: { + margin: '0 auto', + }, + icon: { + width: '100%', + height: '100%', + objectFit: 'contain', + }, + actionIcon: { + alignSelf: 'end', + }, +})); diff --git a/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx index 839c0fe41..4c760ee06 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/BehaviourTab/BehaviourTab.tsx @@ -15,32 +15,16 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => { } label="On click url" - placeholder="Override the default service url when clicking on the service" + description="Overrides the service URL when clicking on the service" + placeholder="URL that should be opened instead when clicking on the service" variant="default" mb="md" - {...form.getInputProps('onClickUrl')} + {...form.getInputProps('behaviour.onClickUrl')} /> - Disables the direct movement of the tile - - } - mb="md" - {...form.getInputProps('isEditModeMovingDisabled')} - /> - - Disables the movement of the tile when moving others - - } - {...form.getInputProps('isEditModeTileFreezed')} + label="Open in new tab" + {...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })} /> ); diff --git a/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx index 4971058c5..397db91f5 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/GeneralTab/GeneralTab.tsx @@ -1,32 +1,52 @@ -import { Tabs, TextInput } from '@mantine/core'; +import { Group, Tabs, Text, TextInput } from '@mantine/core'; import { UseFormReturnType } from '@mantine/form'; import { IconCursorText, IconLink } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { ServiceType } from '../../../../../../types/service'; +import { EditServiceModalTab } from '../type'; interface GeneralTabProps { form: UseFormReturnType ServiceType>; + openTab: (tab: EditServiceModalTab) => void; } -export const GeneralTab = ({ form }: GeneralTabProps) => { +export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { const { t } = useTranslation(''); return ( } label="Service name" + description="Used for displaying the service on the dashboard" placeholder="My example service" variant="default" mb="md" - required + withAsterisk {...form.getInputProps('name')} /> } label="Service url" + description={ + + URL that will be opened when clicking on the service. Can be overwritten using + openTab('behaviour')} + variant="link" + span + style={{ + cursor: 'pointer', + }} + > + {' '} + on click URL{' '} + + when using external URLs to enhance security. + + } placeholder="https://google.com" variant="default" - required + withAsterisk {...form.getInputProps('url')} /> diff --git a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx index 0e2176d07..5087f49c7 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx @@ -1,21 +1,9 @@ /* eslint-disable @next/next/no-img-element */ -import { - Alert, - Card, - Group, - PasswordInput, - Select, - SelectItem, - Space, - Text, - TextInput, -} from '@mantine/core'; +import { Group, Select, SelectItem, Text } from '@mantine/core'; import { UseFormReturnType } from '@mantine/form'; -import { IconKey, IconUser } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { forwardRef, useState } from 'react'; -import { ServiceType } from '../../../../../../../../../types/service'; -import { TextExplanation } from '../TextExplanation/TextExplanation'; +import { ServiceType } from '../../../../../../../../types/service'; interface IntegrationSelectorProps { form: UseFormReturnType ServiceType>; @@ -62,11 +50,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { return ( <> - - - { - return ( - - You can optionally connect your services using integrations. Integration elements on your - dashboard will communicate to your services using the configuration below. - - ); -}; diff --git a/src/components/Dashboard/Modals/EditService/Tabs/NetworkTab/NetworkTab.tsx b/src/components/Dashboard/Modals/EditService/Tabs/NetworkTab/NetworkTab.tsx index 42880aeeb..d2b3da8dd 100644 --- a/src/components/Dashboard/Modals/EditService/Tabs/NetworkTab/NetworkTab.tsx +++ b/src/components/Dashboard/Modals/EditService/Tabs/NetworkTab/NetworkTab.tsx @@ -14,6 +14,7 @@ export const NetworkTab = ({ form }: NetworkTabProps) => { { void; } -export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps) => { - return ( +export const SelectorBackArrow = ({ onClickBack }: SelectorBackArrowProps) => ( } onClick={onClickBack} @@ -18,4 +17,3 @@ export const SelectorBackArrow = ({ title, onClickBack }: SelectorBackArrowProps See all available elements ); -}; diff --git a/src/components/Settings/CommonSettings.tsx b/src/components/Settings/Common/CommonSettings.tsx similarity index 57% rename from src/components/Settings/CommonSettings.tsx rename to src/components/Settings/Common/CommonSettings.tsx index cdbef53f7..8923c3498 100644 --- a/src/components/Settings/CommonSettings.tsx +++ b/src/components/Settings/Common/CommonSettings.tsx @@ -1,10 +1,10 @@ import { Space, Stack, Text } from '@mantine/core'; -import { useConfigContext } from '../../config/provider'; -import ConfigChanger from '../Config/ConfigChanger'; -import ConfigActions from './Common/ConfigActions'; -import LanguageSelect from './Common/LanguageSelect'; -import { SearchEngineSelector } from './Common/SearchEngineSelector'; -import { SearchNewTabSwitch } from './Common/SearchNewTabSwitch'; +import { useConfigContext } from '../../../config/provider'; +import ConfigChanger from '../../Config/ConfigChanger'; +import ConfigActions from './Config/ConfigActions'; +import LanguageSelect from './Language/LanguageSelect'; +import { SearchEngineSelector } from './SearchEngine/SearchEngineSelector'; +import { SearchNewTabSwitch } from './SearchNewTabSwitch'; export default function CommonSettings() { const { config } = useConfigContext(); diff --git a/src/components/Settings/Common/ConfigActions.tsx b/src/components/Settings/Common/Config/ConfigActions.tsx similarity index 61% rename from src/components/Settings/Common/ConfigActions.tsx rename to src/components/Settings/Common/Config/ConfigActions.tsx index f8645efaf..b045ff030 100644 --- a/src/components/Settings/Common/ConfigActions.tsx +++ b/src/components/Settings/Common/Config/ConfigActions.tsx @@ -1,13 +1,13 @@ -import { Button, Center, Group } from '@mantine/core'; +import { ActionIcon, Center, createStyles, Flex, Text, useMantineTheme } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; -import { IconCheck, IconDownload, IconPlus, IconTrash, IconX } from '@tabler/icons'; +import { IconCheck, IconCopy, IconDownload, IconTrash, IconX } from '@tabler/icons'; import { useMutation } from '@tanstack/react-query'; import fileDownload from 'js-file-download'; import { useTranslation } from 'next-i18next'; -import { useConfigContext } from '../../../config/provider'; -import Tip from '../../layout/Tip'; -import { CreateConfigCopyModal } from './ConfigActions/CreateCopyModal'; +import { useConfigContext } from '../../../../config/provider'; +import Tip from '../../../layout/Tip'; +import { CreateConfigCopyModal } from './CreateCopyModal'; export default function ConfigActions() { const { t } = useTranslation(['settings/general/config-changer', 'settings/common']); @@ -26,6 +26,9 @@ export default function ConfigActions() { await mutateAsync(); }; + const { classes } = useStyles(); + const { colors } = useMantineTheme(); + return ( <> - - } - variant="default" - onClick={handleDownload} - > - {t('buttons.download')} - - } - variant="default" + + + + {t('buttons.download')} + + - {t('buttons.delete.text')} - - } - variant="default" - onClick={createCopyModal.open} - > - {t('buttons.saveCopy')} - - + + {t('buttons.delete.text')} + + + + {t('buttons.saveCopy')} + + {t('settings/common:tips.configTip')} @@ -67,6 +63,23 @@ export default function ConfigActions() { ); } +const useStyles = createStyles(() => ({ + actionIcon: { + width: 'auto', + height: 'auto', + maxWidth: 'auto', + maxHeight: 'auto', + flexGrow: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + rowGap: 10, + padding: 10, + }, +})); + const useDeleteConfigMutation = (configName: string) => { const { t } = useTranslation(['settings/general/config-changer']); diff --git a/src/components/Settings/Common/ConfigActions/CreateCopyModal.tsx b/src/components/Settings/Common/Config/CreateCopyModal.tsx similarity index 100% rename from src/components/Settings/Common/ConfigActions/CreateCopyModal.tsx rename to src/components/Settings/Common/Config/CreateCopyModal.tsx diff --git a/src/components/Settings/Credits.tsx b/src/components/Settings/Common/Credits.tsx similarity index 95% rename from src/components/Settings/Credits.tsx rename to src/components/Settings/Common/Credits.tsx index 6b935efc7..677f283e3 100644 --- a/src/components/Settings/Credits.tsx +++ b/src/components/Settings/Common/Credits.tsx @@ -2,7 +2,7 @@ import { Group, ActionIcon, Anchor, Text } from '@mantine/core'; import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; -import { CURRENT_VERSION } from '../../../data/constants'; +import { CURRENT_VERSION } from '../../../../data/constants'; export default function Credits() { const { t } = useTranslation('settings/common'); diff --git a/src/components/Settings/Common/LanguageSelect.tsx b/src/components/Settings/Common/Language/LanguageSelect.tsx similarity index 97% rename from src/components/Settings/Common/LanguageSelect.tsx rename to src/components/Settings/Common/Language/LanguageSelect.tsx index f8a51334a..1d2d2be94 100644 --- a/src/components/Settings/Common/LanguageSelect.tsx +++ b/src/components/Settings/Common/Language/LanguageSelect.tsx @@ -5,7 +5,7 @@ import { forwardRef, useState } from 'react'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { getCookie, setCookie } from 'cookies-next'; -import { getLanguageByCode, Language } from '../../../tools/language'; +import { getLanguageByCode, Language } from '../../../../tools/language'; export default function LanguageSelect() { const { t, i18n } = useTranslation('settings/general/internationalization'); diff --git a/src/components/Settings/Common/SearchEngineSelector.tsx b/src/components/Settings/Common/SearchEngine/SearchEngineSelector.tsx similarity index 95% rename from src/components/Settings/Common/SearchEngineSelector.tsx rename to src/components/Settings/Common/SearchEngine/SearchEngineSelector.tsx index 5f25a1044..3e9fe1d4d 100644 --- a/src/components/Settings/Common/SearchEngineSelector.tsx +++ b/src/components/Settings/Common/SearchEngine/SearchEngineSelector.tsx @@ -2,13 +2,12 @@ import { Alert, Paper, SegmentedControl, Space, Stack, TextInput, Title } from ' import { IconInfoCircle } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; import { CommonSearchEngineCommonSettingsType, SearchEngineCommonSettingsType, -} from '../../../types/settings'; -import Tip from '../../layout/Tip'; +} from '../../../../types/settings'; import { SearchNewTabSwitch } from './SearchNewTabSwitch'; interface Props { diff --git a/src/components/Settings/Common/SearchNewTabSwitch.tsx b/src/components/Settings/Common/SearchEngine/SearchNewTabSwitch.tsx similarity index 86% rename from src/components/Settings/Common/SearchNewTabSwitch.tsx rename to src/components/Settings/Common/SearchEngine/SearchNewTabSwitch.tsx index a3d5cdc72..7391841cc 100644 --- a/src/components/Settings/Common/SearchNewTabSwitch.tsx +++ b/src/components/Settings/Common/SearchEngine/SearchNewTabSwitch.tsx @@ -1,9 +1,9 @@ import { Switch } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; -import { SearchEngineCommonSettingsType } from '../../../types/settings'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; +import { SearchEngineCommonSettingsType } from '../../../../types/settings'; interface SearchNewTabSwitchProps { defaultValue: boolean | undefined; diff --git a/src/components/Settings/CustomizationSettings.tsx b/src/components/Settings/Customization/CustomizationSettings.tsx similarity index 61% rename from src/components/Settings/CustomizationSettings.tsx rename to src/components/Settings/Customization/CustomizationSettings.tsx index aa553b0e1..585389291 100644 --- a/src/components/Settings/CustomizationSettings.tsx +++ b/src/components/Settings/Customization/CustomizationSettings.tsx @@ -1,15 +1,15 @@ import { Stack } from '@mantine/core'; -import { useConfigContext } from '../../config/provider'; -import { ColorSelector } from './Customization/ColorSelector'; -import { BackgroundChanger } from './Customization/BackgroundChanger'; -import { CustomCssChanger } from './Customization/CustomCssChanger'; -import { FaviconChanger } from './Customization/FaviconChanger'; -import { LogoImageChanger } from './Customization/LogoImageChanger'; -import { MetaTitleChanger } from './Customization/MetaTitleChanger'; -import { PageTitleChanger } from './Customization/PageTitleChanger'; -import { OpacitySelector } from './Customization/OpacitySelector'; -import { ShadeSelector } from './Customization/ShadeSelector'; -import { LayoutSelector } from './Customization/LayoutSelector'; +import { useConfigContext } from '../../../config/provider'; +import { ColorSelector } from './Theme/ColorSelector'; +import { BackgroundChanger } from './Meta/BackgroundChanger'; +import { CustomCssChanger } from './Theme/CustomCssChanger'; +import { FaviconChanger } from './Meta/FaviconChanger'; +import { LogoImageChanger } from './Meta/LogoImageChanger'; +import { MetaTitleChanger } from './Meta/MetaTitleChanger'; +import { PageTitleChanger } from './Meta/PageTitleChanger'; +import { OpacitySelector } from './Theme/OpacitySelector'; +import { ShadeSelector } from './Theme/ShadeSelector'; +import { LayoutSelector } from './Layout/LayoutSelector'; export default function CustomizationSettings() { const { config } = useConfigContext(); diff --git a/src/components/Settings/Customization/Layout/LayoutSelector.tsx b/src/components/Settings/Customization/Layout/LayoutSelector.tsx new file mode 100644 index 000000000..af066e0bd --- /dev/null +++ b/src/components/Settings/Customization/Layout/LayoutSelector.tsx @@ -0,0 +1,167 @@ +import { + ActionIcon, + Center, + Checkbox, + createStyles, + Flex, + Group, + Paper, + Stack, + Text, + Title, + useMantineTheme, +} from '@mantine/core'; +import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; +import { CustomizationSettingsType } from '../../../../types/settings'; +import { Logo } from '../../../layout/Logo'; + +interface LayoutSelectorProps { + defaultLayout: CustomizationSettingsType['layout'] | undefined; +} + +// TODO: add translations +export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => { + const { classes } = useStyles(); + + const { name: configName } = useConfigContext(); + const updateConfig = useConfigStore((x) => x.updateConfig); + + const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true); + const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true); + const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false); + const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false); + const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false); + + const { colors, colorScheme } = useMantineTheme(); + + if (!configName) return null; + + const handleChange = ( + key: keyof CustomizationSettingsType['layout'], + event: ChangeEvent, + setState: Dispatch> + ) => { + const value = event.target.checked; + setState(value); + updateConfig(configName, (prev) => { + const { layout } = prev.settings.customization; + + layout[key] = value; + + return { + ...prev, + settings: { + ...prev.settings, + customization: { + ...prev.settings.customization, + layout, + }, + }, + }; + }); + }; + + return ( + + Dashboard layout + + + + + + {searchBar ? ( + + ) : null} + {docker ? : null} + + + + + + {leftSidebar && ( + + + Sidebar + + Only for + + services & + integrations + + + + )} + + + Main + + Cannot be turned of. + + + + {rightSidebar && ( + + + Sidebar + + Only for + + services & + integrations + + + + )} + + + + handleChange('enabledLeftSidebar', ev, setLeftSidebar)} + /> + handleChange('enabledRightSidebar', ev, setRightSidebar)} + /> + handleChange('enabledSearchbar', ev, setSearchBar)} + /> + handleChange('enabledDocker', ev, setDocker)} + /> + handleChange('enabledPing', ev, setPing)} + /> + + + ); +}; + +const useStyles = createStyles((theme) => ({ + primaryWrapper: { + flexGrow: 2, + }, + secondaryWrapper: { + flexGrow: 1, + maxWidth: 100, + }, +})); diff --git a/src/components/Settings/Customization/LayoutSelector.tsx b/src/components/Settings/Customization/LayoutSelector.tsx deleted file mode 100644 index f1e072f89..000000000 --- a/src/components/Settings/Customization/LayoutSelector.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - Box, - Center, - Checkbox, - createStyles, - Group, - Paper, - Stack, - Text, - Title, -} from '@mantine/core'; -import { IconBrandDocker, IconLayout, IconSearch } from '@tabler/icons'; -import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; -import { CustomizationSettingsType } from '../../../types/settings'; -import { Logo } from '../../layout/Logo'; - -interface LayoutSelectorProps { - defaultLayout: CustomizationSettingsType['layout'] | undefined; -} - -// TODO: add translations -export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => { - const { classes } = useStyles(); - - const { name: configName } = useConfigContext(); - const updateConfig = useConfigStore((x) => x.updateConfig); - - const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true); - const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true); - const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false); - const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false); - const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false); - - if (!configName) return null; - - const handleChange = ( - key: keyof CustomizationSettingsType['layout'], - event: ChangeEvent, - setState: Dispatch> - ) => { - const value = event.target.checked; - setState(value); - updateConfig(configName, (prev) => { - const layout = prev.settings.customization.layout; - - layout[key] = value; - - return { - ...prev, - settings: { - ...prev.settings, - customization: { - ...prev.settings.customization, - layout, - }, - }, - }; - }); - }; - - return ( - - - - - Dashboard layout - - - - You can adjust the layout of the Dashboard to your preferences. The main are cannot be - turned on or off - - - - - - - {searchBar ? ( - - - - - Search - - - - ) : null} - {docker ? : null} - - - - - - {leftSidebar && ( - - - Sidebar - - - )} - - - Main - - Can be used for categories, - - services and integrations - - - - {rightSidebar && ( - - - Sidebar - - - )} - - - - handleChange('enabledLeftSidebar', ev, setLeftSidebar)} - /> - handleChange('enabledRightSidebar', ev, setRightSidebar)} - /> - handleChange('enabledSearchbar', ev, setSearchBar)} - /> - handleChange('enabledDocker', ev, setDocker)} - /> - handleChange('enabledPing', ev, setPing)} - /> - - - - ); -}; - -const useStyles = createStyles((theme) => ({ - main: { - flexGrow: 1, - }, - box: { - backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1], - borderRadius: theme.radius.md, - }, -})); diff --git a/src/components/Settings/Customization/BackgroundChanger.tsx b/src/components/Settings/Customization/Meta/BackgroundChanger.tsx similarity index 88% rename from src/components/Settings/Customization/BackgroundChanger.tsx rename to src/components/Settings/Customization/Meta/BackgroundChanger.tsx index 2addf76d9..5c3efabd2 100644 --- a/src/components/Settings/Customization/BackgroundChanger.tsx +++ b/src/components/Settings/Customization/Meta/BackgroundChanger.tsx @@ -1,8 +1,8 @@ import { TextInput } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface BackgroundChangerProps { defaultValue: string | undefined; @@ -17,7 +17,7 @@ export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => { if (!configName) return null; const handleChange: ChangeEventHandler = (ev) => { - const value = ev.currentTarget.value; + const { value } = ev.currentTarget; const backgroundImageUrl = value.trim().length === 0 ? undefined : value; setBackgroundImageUrl(backgroundImageUrl); updateConfig(configName, (prev) => ({ diff --git a/src/components/Settings/Customization/FaviconChanger.tsx b/src/components/Settings/Customization/Meta/FaviconChanger.tsx similarity index 87% rename from src/components/Settings/Customization/FaviconChanger.tsx rename to src/components/Settings/Customization/Meta/FaviconChanger.tsx index b99347959..7bb71f815 100644 --- a/src/components/Settings/Customization/FaviconChanger.tsx +++ b/src/components/Settings/Customization/Meta/FaviconChanger.tsx @@ -1,8 +1,8 @@ import { TextInput } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface FaviconChangerProps { defaultValue: string | undefined; @@ -17,7 +17,7 @@ export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => { if (!configName) return null; const handleChange: ChangeEventHandler = (ev) => { - const value = ev.currentTarget.value; + const { value } = ev.currentTarget; const faviconUrl = value.trim().length === 0 ? undefined : value; setFaviconUrl(faviconUrl); updateConfig(configName, (prev) => ({ diff --git a/src/components/Settings/Customization/LogoImageChanger.tsx b/src/components/Settings/Customization/Meta/LogoImageChanger.tsx similarity index 87% rename from src/components/Settings/Customization/LogoImageChanger.tsx rename to src/components/Settings/Customization/Meta/LogoImageChanger.tsx index 3ab1db7fe..0f45de7d5 100644 --- a/src/components/Settings/Customization/LogoImageChanger.tsx +++ b/src/components/Settings/Customization/Meta/LogoImageChanger.tsx @@ -1,8 +1,8 @@ import { TextInput } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface LogoImageChangerProps { defaultValue: string | undefined; @@ -17,7 +17,7 @@ export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => { if (!configName) return null; const handleChange: ChangeEventHandler = (ev) => { - const value = ev.currentTarget.value; + const { value } = ev.currentTarget; const logoImageSrc = value.trim().length === 0 ? undefined : value; setLogoImageSrc(logoImageSrc); updateConfig(configName, (prev) => ({ diff --git a/src/components/Settings/Customization/MetaTitleChanger.tsx b/src/components/Settings/Customization/Meta/MetaTitleChanger.tsx similarity index 87% rename from src/components/Settings/Customization/MetaTitleChanger.tsx rename to src/components/Settings/Customization/Meta/MetaTitleChanger.tsx index 79ef71f68..430eee07f 100644 --- a/src/components/Settings/Customization/MetaTitleChanger.tsx +++ b/src/components/Settings/Customization/Meta/MetaTitleChanger.tsx @@ -1,8 +1,8 @@ import { TextInput } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface MetaTitleChangerProps { defaultValue: string | undefined; @@ -18,7 +18,7 @@ export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => { if (!configName) return null; const handleChange: ChangeEventHandler = (ev) => { - const value = ev.currentTarget.value; + const { value } = ev.currentTarget; const metaTitle = value.trim().length === 0 ? undefined : value; setMetaTitle(metaTitle); updateConfig(configName, (prev) => ({ diff --git a/src/components/Settings/Customization/PageTitleChanger.tsx b/src/components/Settings/Customization/Meta/PageTitleChanger.tsx similarity index 87% rename from src/components/Settings/Customization/PageTitleChanger.tsx rename to src/components/Settings/Customization/Meta/PageTitleChanger.tsx index d4145e645..65c9eed76 100644 --- a/src/components/Settings/Customization/PageTitleChanger.tsx +++ b/src/components/Settings/Customization/Meta/PageTitleChanger.tsx @@ -1,8 +1,8 @@ import { TextInput } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface PageTitleChangerProps { defaultValue: string | undefined; @@ -18,7 +18,7 @@ export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => { if (!configName) return null; const handleChange: ChangeEventHandler = (ev) => { - const value = ev.currentTarget.value; + const { value } = ev.currentTarget; const pageTitle = value.trim().length === 0 ? undefined : value; setPageTitle(pageTitle); updateConfig(configName, (prev) => ({ diff --git a/src/components/Settings/Customization/ColorSelector.tsx b/src/components/Settings/Customization/Theme/ColorSelector.tsx similarity index 90% rename from src/components/Settings/Customization/ColorSelector.tsx rename to src/components/Settings/Customization/Theme/ColorSelector.tsx index 7e98969bc..053a7d386 100644 --- a/src/components/Settings/Customization/ColorSelector.tsx +++ b/src/components/Settings/Customization/Theme/ColorSelector.tsx @@ -1,4 +1,3 @@ -import React, { useState } from 'react'; import { ColorSwatch, Grid, @@ -8,11 +7,12 @@ import { Text, useMantineTheme, } from '@mantine/core'; -import { useTranslation } from 'next-i18next'; -import { useColorTheme } from '../../../tools/color'; import { useDisclosure } from '@mantine/hooks'; -import { useConfigStore } from '../../../config/store'; -import { useConfigContext } from '../../../config/provider'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; +import { useColorTheme } from '../../../../tools/color'; interface ColorControlProps { defaultValue: MantineTheme['primaryColor'] | undefined; @@ -40,7 +40,7 @@ export function ColorSelector({ type, defaultValue }: ColorControlProps) { if (type === 'primary') setPrimaryColor(color); else setSecondaryColor(color); updateConfig(configName, (prev) => { - const colors = prev.settings.customization.colors; + const { colors } = prev.settings.customization; colors[type] = color; return { ...prev, diff --git a/src/components/Settings/Customization/CustomCssChanger.tsx b/src/components/Settings/Customization/Theme/CustomCssChanger.tsx similarity index 87% rename from src/components/Settings/Customization/CustomCssChanger.tsx rename to src/components/Settings/Customization/Theme/CustomCssChanger.tsx index 5a7393e34..5b8db8de1 100644 --- a/src/components/Settings/Customization/CustomCssChanger.tsx +++ b/src/components/Settings/Customization/Theme/CustomCssChanger.tsx @@ -1,8 +1,8 @@ import { Textarea } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { ChangeEventHandler, useState } from 'react'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface CustomCssChangerProps { defaultValue: string | undefined; @@ -17,7 +17,7 @@ export const CustomCssChanger = ({ defaultValue }: CustomCssChangerProps) => { if (!configName) return null; const handleChange: ChangeEventHandler = (ev) => { - const value = ev.currentTarget.value; + const { value } = ev.currentTarget; const customCss = value.trim().length === 0 ? undefined : value; setCustomCss(customCss); updateConfig(configName, (prev) => ({ diff --git a/src/components/Settings/Customization/OpacitySelector.tsx b/src/components/Settings/Customization/Theme/OpacitySelector.tsx similarity index 86% rename from src/components/Settings/Customization/OpacitySelector.tsx rename to src/components/Settings/Customization/Theme/OpacitySelector.tsx index fc858aea0..cc7888af6 100644 --- a/src/components/Settings/Customization/OpacitySelector.tsx +++ b/src/components/Settings/Customization/Theme/OpacitySelector.tsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react'; -import { Text, Slider, Stack } from '@mantine/core'; +import { Slider, Stack, Text } from '@mantine/core'; import { useTranslation } from 'next-i18next'; -import { useConfigContext } from '../../../config/provider'; -import { useConfigStore } from '../../../config/store'; +import { useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; interface OpacitySelectorProps { defaultValue: number | undefined; diff --git a/src/components/Settings/Customization/ShadeSelector.tsx b/src/components/Settings/Customization/Theme/ShadeSelector.tsx similarity index 92% rename from src/components/Settings/Customization/ShadeSelector.tsx rename to src/components/Settings/Customization/Theme/ShadeSelector.tsx index 61e710517..5611581ae 100644 --- a/src/components/Settings/Customization/ShadeSelector.tsx +++ b/src/components/Settings/Customization/Theme/ShadeSelector.tsx @@ -1,19 +1,19 @@ -import React, { useState } from 'react'; import { ColorSwatch, + Grid, Group, + MantineTheme, Popover, + Stack, Text, useMantineTheme, - MantineTheme, - Stack, - Grid, } from '@mantine/core'; -import { useTranslation } from 'next-i18next'; -import { useColorTheme } from '../../../tools/color'; import { useDisclosure } from '@mantine/hooks'; -import { useConfigStore } from '../../../config/store'; -import { useConfigContext } from '../../../config/provider'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; +import { useColorTheme } from '../../../../tools/color'; interface ShadeSelectorProps { defaultValue: MantineTheme['primaryShade'] | undefined; diff --git a/src/components/Settings/SettingsDrawer.tsx b/src/components/Settings/SettingsDrawer.tsx index b21c98da5..3fef9e2ef 100644 --- a/src/components/Settings/SettingsDrawer.tsx +++ b/src/components/Settings/SettingsDrawer.tsx @@ -4,9 +4,9 @@ import { useState } from 'react'; import { IconSettings } from '@tabler/icons'; import { useTranslation } from 'next-i18next'; -import CustomizationSettings from './CustomizationSettings'; -import CommonSettings from './CommonSettings'; -import Credits from './Credits'; +import CustomizationSettings from './Customization/CustomizationSettings'; +import CommonSettings from './Common/CommonSettings'; +import Credits from './Common/Credits'; function SettingsMenu() { const { t } = useTranslation('settings/common'); diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 7ea0e2e7e..1a4712700 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -3,7 +3,7 @@ import { useConfigContext } from '../../config/provider'; import { Background } from './Background'; import { Footer } from './Footer'; import { Header } from './Header/Header'; -import { Head } from './Head/Head'; +import { Head } from './Header/Meta/Head'; const useStyles = createStyles(() => ({})); diff --git a/src/components/layout/Logo.tsx b/src/components/layout/Logo.tsx index 69594133a..ef943ca43 100644 --- a/src/components/layout/Logo.tsx +++ b/src/components/layout/Logo.tsx @@ -14,7 +14,7 @@ export function Logo({ size = 'md', withoutText = false }: LogoProps) { return ( {withoutText ? null : ( { const { t } = useTranslation('layout/add-service-app-shelf'); diff --git a/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx new file mode 100644 index 000000000..0f52356bf --- /dev/null +++ b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx @@ -0,0 +1,36 @@ +import { Button, Popover, Tooltip, Text } from '@mantine/core'; +import { IconEditCircle, IconEditCircleOff } from '@tabler/icons'; +import { useTranslation } from 'next-i18next'; + +import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore'; + +export const ToggleEditModeAction = () => { + const { t } = useTranslation('layout/header/actions/toggle-edit-mode'); + + const { enabled, toggleEditMode } = useEditModeStore(); + + return ( + + + + toggleEditMode()} + leftIcon={enabled ? : } + variant="default" + radius="md" + color="blue" + style={{ height: 'auto', alignSelf: 'stretch' }} + > + {enabled ? t('button.enabled') : t('button.disabled')} + + + + + {t('popover.title')} + {t('popover.text')} + + + + + ); +}; diff --git a/src/components/layout/header/Header.tsx b/src/components/layout/header/Header.tsx index ca3fdca74..68185c409 100644 --- a/src/components/layout/header/Header.tsx +++ b/src/components/layout/header/Header.tsx @@ -1,15 +1,12 @@ -import { Box, createStyles, Group, Header as MantineHeader, useMantineTheme } from '@mantine/core'; -import { useViewportSize } from '@mantine/hooks'; -import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem'; - -import DockerMenuButton from '../../../modules/docker/DockerModule'; -import { Search } from './Search'; +import { Box, createStyles, Group, Header as MantineHeader, Text } from '@mantine/core'; +import { openContextModal } from '@mantine/modals'; +import { useConfigContext } from '../../../config/provider'; import { Logo } from '../Logo'; import { useCardStyles } from '../useCardStyles'; -import { SettingsMenu } from './SettingsMenu'; -import { ToolsMenu } from './ToolsMenu'; import { AddElementAction } from './Actions/AddElementAction/AddElementAction'; -import { ViewToggleButton } from '../../Dashboard/Views/ViewToggleButton'; +import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode'; +import { Search } from './Search'; +import { SettingsMenu } from './SettingsMenu'; export const HeaderHeight = 64; @@ -17,6 +14,8 @@ export function Header(props: any) { const { classes } = useStyles(); const { classes: cardClasses } = useCardStyles(); + const { config } = useConfigContext(); + return ( @@ -24,11 +23,24 @@ export function Header(props: any) { + { + openContextModal({ + modal: 'changeTilePosition', + title: 'Change tile position', + innerProps: { + tile: config?.services[0], + }, + }); + }} + variant="link" + > + Test: Open Change Pos Modal + + - - - + diff --git a/src/components/layout/Head/Head.tsx b/src/components/layout/header/Meta/Head.tsx similarity index 94% rename from src/components/layout/Head/Head.tsx rename to src/components/layout/header/Meta/Head.tsx index f85f28efc..a0cb45691 100644 --- a/src/components/layout/Head/Head.tsx +++ b/src/components/layout/header/Meta/Head.tsx @@ -2,7 +2,7 @@ import React from 'react'; import NextHead from 'next/head'; import { SafariStatusBarStyle } from './SafariStatusBarStyle'; -import { useConfigContext } from '../../../config/provider'; +import { useConfigContext } from '../../../../config/provider'; export function Head() { const { config } = useConfigContext(); diff --git a/src/components/layout/Head/SafariStatusBarStyle.tsx b/src/components/layout/header/Meta/SafariStatusBarStyle.tsx similarity index 100% rename from src/components/layout/Head/SafariStatusBarStyle.tsx rename to src/components/layout/header/Meta/SafariStatusBarStyle.tsx diff --git a/src/components/layout/header/Search.tsx b/src/components/layout/header/Search.tsx index e9b5f6a84..899d76f85 100644 --- a/src/components/layout/header/Search.tsx +++ b/src/components/layout/header/Search.tsx @@ -18,7 +18,7 @@ import { useTranslation } from 'next-i18next'; import React, { forwardRef, useEffect, useRef, useState } from 'react'; import SmallServiceItem from '../../AppShelf/SmallServiceItem'; import Tip from '../Tip'; -import { searchUrls } from '../../Settings/Common/SearchEngineSelector'; +import { searchUrls } from '../../Settings/Common/SearchEngine/SearchEngineSelector'; import { useConfigContext } from '../../../config/provider'; import { OverseerrMediaDisplay } from '../../../modules/common'; import { IModule } from '../../../modules/ModuleTypes'; diff --git a/src/components/layout/header/ToolsMenu.tsx b/src/components/layout/header/ToolsMenu.tsx deleted file mode 100644 index 77a3b45c4..000000000 --- a/src/components/layout/header/ToolsMenu.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ActionIcon, Menu, Text } from '@mantine/core'; -import { IconAxe } from '@tabler/icons'; -import { useTranslation } from 'next-i18next'; -import DockerMenuButton from '../../../modules/docker/DockerModule'; - -export const ToolsMenu = () => { - const { t } = useTranslation('layout/tools'); - return ( - - - - - - - - {/* TODO: Implement check to display fallback when no tools */} - - - - {t('fallback.title')} - - - - - ); -}; diff --git a/src/modules/docker/ContainerActionBar.tsx b/src/modules/docker/ContainerActionBar.tsx index 0dd5e9ff5..6805f18c2 100644 --- a/src/modules/docker/ContainerActionBar.tsx +++ b/src/modules/docker/ContainerActionBar.tsx @@ -16,9 +16,11 @@ import Dockerode from 'dockerode'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; import { TFunction } from 'react-i18next'; -import { AddAppShelfItemForm } from '../../components/AppShelf/AddAppShelfItem'; +import { v4 as uuidv4 } from 'uuid'; import { tryMatchService } from '../../tools/addToHomarr'; +import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions'; import { useConfig } from '../../tools/state'; +import { ServiceType } from '../../types/service'; let t: TFunction<'modules/docker', undefined>; @@ -160,21 +162,40 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction radius="md" disabled={selected.length === 0 || selected.length > 1} onClick={() => { - openModal({ - size: 'xl', - modalId: selected.at(0)!.Id, - radius: 'md', - title: t('actionBar.addService.title'), - zIndex: 500, - children: ( - closeModal(selected.at(0)!.Id)} - message={t('actionBar.addService.message')} - {...tryMatchService(selected.at(0)!)} - /> - ), + const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`; + openContextModalGeneric<{ service: ServiceType }>({ + modal: 'editService', + innerProps: { + service: { + id: uuidv4(), + name: selected[0].Names[0], + url: containerUrl, + appearance: { + iconUrl: '/imgs/logo/logo.png', // TODO: find icon automatically + }, + network: { + enabledStatusChecker: false, + okStatus: [], + }, + behaviour: { + isOpeningNewTab: true, + onClickUrl: '', + }, + area: { + type: 'wrapper', // TODO: Set the wrapper automatically + }, + shape: { + location: { + x: 0, + y: 0, + }, + size: { + height: 1, + width: 1, + }, + }, + }, + }, }); }} > diff --git a/src/modules/torrents/TorrentsModule.tsx b/src/modules/torrents/TorrentsModule.tsx index cd010992c..714bcb618 100644 --- a/src/modules/torrents/TorrentsModule.tsx +++ b/src/modules/torrents/TorrentsModule.tsx @@ -20,7 +20,6 @@ import { NormalizedTorrent } from '@ctrl/shared-torrent'; import { useTranslation } from 'next-i18next'; import { IModule } from '../ModuleTypes'; import { useConfig } from '../../tools/state'; -import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem'; import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval'; import { humanFileSize } from '../../tools/humanFileSize'; @@ -93,7 +92,6 @@ export default function TorrentsComponent() { {t('card.errors.noDownloadClients.title')} {t('card.errors.noDownloadClients.text')} - ); diff --git a/src/modules/torrents/TotalDownloadsModule.tsx b/src/modules/torrents/TotalDownloadsModule.tsx index 95f64852e..a0fda21cc 100644 --- a/src/modules/torrents/TotalDownloadsModule.tsx +++ b/src/modules/torrents/TotalDownloadsModule.tsx @@ -8,7 +8,6 @@ import { useTranslation } from 'next-i18next'; import { Datum, ResponsiveLine } from '@nivo/line'; import { useListState } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; -import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem'; import { useConfig } from '../../tools/state'; import { humanFileSize } from '../../tools/humanFileSize'; import { IModule } from '../ModuleTypes'; @@ -84,11 +83,6 @@ export default function TotalDownloadsComponent() { {t('card.errors.noDownloadClients.title')} - {t('card.errors.noDownloadClients.text')} diff --git a/src/modules/usenet/UsenetModule.tsx b/src/modules/usenet/UsenetModule.tsx index 6545889ee..b733ae34b 100644 --- a/src/modules/usenet/UsenetModule.tsx +++ b/src/modules/usenet/UsenetModule.tsx @@ -22,7 +22,6 @@ import { UsenetHistoryList } from './UsenetHistoryList'; import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType'; import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api'; import { humanFileSize } from '../../tools/humanFileSize'; -import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem'; dayjs.extend(duration); @@ -49,7 +48,6 @@ export const UsenetComponent: FunctionComponent = () => { {t('card.errors.noDownloadClients.title')} {t('card.errors.noDownloadClients.text')} - ); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 5930e7e2d..5448b0272 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -11,6 +11,7 @@ import Head from 'next/head'; import { useState } from 'react'; import { IntegrationsEditModal } from '../components/Dashboard/Tiles/IntegrationsEditModal'; import { IntegrationRemoveModal } from '../components/Dashboard/Tiles/IntegrationRemoveModal'; +import { ChangePositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangePositionModal'; import { EditServiceModal } from '../components/Dashboard/Modals/EditService/EditServiceModal'; import { SelectElementModal } from '../components/Dashboard/Modals/SelectElement/SelectElementModal'; import { ConfigProvider } from '../config/provider'; @@ -91,6 +92,7 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) { integrationRemove: IntegrationRemoveModal, integrationChangePosition: IntegrationChangePositionModal, categoryEditModal: CategoryEditModal, + changeTilePosition: ChangePositionModal, }} > diff --git a/src/tools/hooks/useRepositoryIconsQuery.ts b/src/tools/hooks/useRepositoryIconsQuery.ts new file mode 100644 index 000000000..e08e096ad --- /dev/null +++ b/src/tools/hooks/useRepositoryIconsQuery.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query'; +import { IconSelectorItem } from '../../types/iconSelector/iconSelectorItem'; + +export const useRepositoryIconsQuery = ({ + url, + converter, +}: { + url: string; + converter: (value: TRepositoryIcon) => IconSelectorItem; +}) => + useQuery({ + queryKey: ['repository-icons', { url }], + queryFn: async () => fetchRepositoryIcons(url), + select(data) { + return data.map(x => converter(x)); + }, + refetchOnWindowFocus: false, + }); + +const fetchRepositoryIcons = + async (url: string): Promise => { + const response = await fetch( + 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png' + ); + return response.json(); +}; diff --git a/src/tools/translation-namespaces.ts b/src/tools/translation-namespaces.ts index 5d155f258..b1fd0ec79 100644 --- a/src/tools/translation-namespaces.ts +++ b/src/tools/translation-namespaces.ts @@ -5,6 +5,7 @@ export const dashboardNamespaces = [ 'layout/app-shelf-menu', 'layout/tools', 'layout/element-selector/selector', + 'layout/header/actions/toggle-edit-mode', 'settings/common', 'settings/general/theme-selector', 'settings/general/config-changer', diff --git a/src/types/iconSelector/iconSelectorItem.ts b/src/types/iconSelector/iconSelectorItem.ts new file mode 100644 index 000000000..6a38b1850 --- /dev/null +++ b/src/types/iconSelector/iconSelectorItem.ts @@ -0,0 +1,3 @@ +export interface IconSelectorItem { + url: string; +} diff --git a/src/types/iconSelector/repositories/walkxcodeIconRepository.ts b/src/types/iconSelector/repositories/walkxcodeIconRepository.ts new file mode 100644 index 000000000..70aab46aa --- /dev/null +++ b/src/types/iconSelector/repositories/walkxcodeIconRepository.ts @@ -0,0 +1,3 @@ +export interface WalkxcodeRepositoryIcon { + name: string; +} diff --git a/src/types/service.ts b/src/types/service.ts index 0c4af3fa0..719189be1 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -11,7 +11,7 @@ export interface ServiceType extends TileBaseType { } interface ServiceBehaviourType { - onClickUrl?: string; + onClickUrl: string; isOpeningNewTab: boolean; }