Add icon picker for service icon

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel Ruwe
2022-12-05 21:43:47 +01:00
parent b7bb1302e4
commit d7bec26ee2
8 changed files with 172 additions and 35 deletions

View File

@@ -1,2 +1,3 @@
export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.10.7';
export const ICON_PICKER_SLICE_LIMIT = 36;

View File

@@ -1,7 +1,7 @@
import Image from 'next/image';
import { Button, createStyles, Group, Stack, Tabs, Text } 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,
@@ -12,7 +12,7 @@ import {
IconPlug,
} from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { hideNotification, showNotification } from '@mantine/notifications';
import Image from 'next/image';
import { ServiceType } from '../../../../types/service';
import { AppearanceTab } from './Tabs/AppereanceTab/AppereanceTab';
import { BehaviourTab } from './Tabs/BehaviourTab/BehaviourTab';

View File

@@ -1,8 +1,9 @@
import { Tabs, TextInput, createStyles } from '@mantine/core';
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, (values: ServiceType) => ServiceType>;
@@ -24,20 +25,35 @@ export const AppearanceTab = ({ form }: AppearanceTabProps) => {
return (
<Tabs.Panel value="appearance" pt="lg">
<Flex gap={5}>
<TextInput
defaultValue={form.values.appearance.iconUrl}
className={classes.textInput}
icon={<Image />}
label="Service Icon"
variant="default"
defaultValue={form.values.appearance.iconUrl}
{...form.getInputProps('appearance.iconUrl')}
withAsterisk
required
{...form.getInputProps('appearance.iconUrl')}
/>
<IconSelector
onChange={(item) =>
form.setValues({
appearance: {
iconUrl: item.url,
},
})
}
/>
</Flex>
</Tabs.Panel>
);
};
const useStyles = createStyles(() => ({
textInput: {
flexGrow: 1,
},
iconImage: {
objectFit: 'contain',
width: 20,

View File

@@ -0,0 +1,108 @@
/* eslint-disable @next/next/no-img-element */
import {
ActionIcon,
createStyles,
Divider,
Flex,
Loader,
Popover,
ScrollArea,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconFlame, 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<WalkxcodeRepositoryIcon>({
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<string>('');
const { classes } = useStyles();
if (isLoading || !data) {
return <Loader />;
}
const filteredItems = searchTerm ? data.filter((x) => x.url.includes(searchTerm)) : data;
const slicedFilteredItems = filteredItems.slice(0, ICON_PICKER_SLICE_LIMIT);
const isTruncated =
slicedFilteredItems.length > 0 && slicedFilteredItems.length !== filteredItems.length;
return (
<Popover width={310}>
<Popover.Target>
<ActionIcon className={classes.actionIcon} size={36} variant="default">
<IconSearch size={20} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<Stack pt={4}>
<TextInput
value={searchTerm}
onChange={(event) => setSearchTerm(event.currentTarget.value)}
placeholder="Search for icons..."
variant="filled"
rightSection={
<ActionIcon onClick={() => setSearchTerm('')}>
<IconX opacity={0.5} size={20} strokeWidth={2} />
</ActionIcon>
}
/>
<ScrollArea style={{ height: 250 }} type="always">
<Flex gap={4} wrap="wrap" pr={15}>
{slicedFilteredItems.map((item) => (
<ActionIcon onClick={() => onChange(item)} size={40} p={3}>
<img className={classes.icon} src={item.url} alt="icon from repository" />
</ActionIcon>
))}
</Flex>
{isTruncated && (
<Stack spacing="xs" pr={15}>
<Divider mt={35} mx="xl" />
<IconFlame className={classes.flameIcon} size={40} opacity={0.6} strokeWidth={1} />
<Title order={6} color="dimmed" align="center">
Search is limited to {ICON_PICKER_SLICE_LIMIT} icons
</Title>
<Text color="dimmed" align="center" size="sm">
To keep things snappy and fast, the search is limited to {ICON_PICKER_SLICE_LIMIT}{' '}
icons. Use the search box to find more icons.
</Text>
</Stack>
)}
</ScrollArea>
</Stack>
</Popover.Dropdown>
</Popover>
);
};
const useStyles = createStyles(() => ({
flameIcon: {
margin: '0 auto',
},
icon: {
width: '100%',
height: '100%',
objectFit: 'contain',
},
actionIcon: {
alignSelf: 'end',
},
}));

View File

@@ -18,30 +18,10 @@ export const BehaviourTab = ({ form }: BehaviourTabProps) => {
placeholder="Override the default service url when clicking on the service"
variant="default"
mb="md"
{...form.getInputProps('onClickUrl')}
{...form.getInputProps('behaviour.onClickUrl')}
/>
<Switch
value="disable_handle"
label="Disable direct moving in edit modus"
description={
<Text color="dimmed" size="sm">
Disables the direct movement of the tile
</Text>
}
mb="md"
{...form.getInputProps('isEditModeMovingDisabled')}
/>
<Switch
value="freze"
label="Freeze tile within edit modus"
description={
<Text color="dimmed" size="sm">
Disables the movement of the tile when moving others
</Text>
}
{...form.getInputProps('isEditModeTileFreezed')}
/>
<Switch label="Open in new tab" {...form.getInputProps('behaviour.isOpeningNewTab')} />
</Tabs.Panel>
);
};

View File

@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { IconSelectorItem } from '../../types/iconSelector/iconSelectorItem';
export const useRepositoryIconsQuery = <TRepositoryIcon extends object>({
url,
converter,
}: {
url: string;
converter: (value: TRepositoryIcon) => IconSelectorItem;
}) =>
useQuery({
queryKey: ['repository-icons', { url }],
queryFn: async () => fetchRepositoryIcons<TRepositoryIcon>(url),
select(data) {
return data.map(x => converter(x));
},
refetchOnWindowFocus: false,
});
const fetchRepositoryIcons =
async <TRepositoryIcon extends object>(url: string): Promise<TRepositoryIcon[]> => {
const response = await fetch(
'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png'
);
return response.json();
};

View File

@@ -0,0 +1,3 @@
export interface IconSelectorItem {
url: string;
}

View File

@@ -0,0 +1,3 @@
export interface WalkxcodeRepositoryIcon {
name: string;
}