Merge branch 'dev' of https://github.com/ajnart/homarr into widget-option-tooltips

This commit is contained in:
Tagaishi
2023-07-30 14:18:09 +02:00
53 changed files with 265 additions and 4195 deletions

View File

@@ -7,12 +7,15 @@ import {
Group,
Image,
ScrollArea,
Divider,
Stack,
Switch,
Text,
TextInput,
Title,
createStyles,
useMantineTheme,
InputProps,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import {
@@ -27,6 +30,7 @@ import { useTranslation } from 'next-i18next';
import { useEffect } from 'react';
import { v4 } from 'uuid';
import { z } from 'zod';
import React from 'react';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
import { IconSelector } from '../../components/IconSelector/IconSelector';
@@ -39,12 +43,18 @@ interface BookmarkItem {
href: string;
iconUrl: string;
openNewTab: boolean;
hideHostname: boolean;
hideIcon: boolean;
}
const definition = defineWidget({
id: 'bookmark',
icon: IconBookmark,
options: {
name: {
type: 'text',
defaultValue: '',
},
items: {
type: 'draggable-editable-list',
defaultValue: [],
@@ -58,6 +68,8 @@ const definition = defineWidget({
href: 'https://homarr.dev',
iconUrl: '/imgs/logo/logo.png',
openNewTab: false,
hideHostname: false,
hideIcon: false,
};
},
itemComponent({ data, onChange, delete: deleteData }) {
@@ -130,6 +142,16 @@ const definition = defineWidget({
label="Open in new tab"
checked={form.values.openNewTab}
/>
<Switch
{...form.getInputProps('hideHostname')}
label="Hide Hostname"
checked={form.values.hideHostname}
/>
<Switch
{...form.getInputProps('hideIcon')}
label="Hide Icon"
checked={form.values.hideIcon}
/>
<Button
onClick={() => deleteData()}
leftIcon={<IconTrash size="1rem" />}
@@ -186,6 +208,7 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
const { t } = useTranslation('modules/bookmark');
const { classes } = useStyles();
const { enabled: isEditModeEnabled } = useEditModeStore();
const { fn, colors, colorScheme } = useMantineTheme();
if (widget.properties.items.length === 0) {
return (
@@ -206,72 +229,126 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
switch (widget.properties.layout) {
case 'autoGrid':
return (
<Box className={classes.grid} display="grid" mr={isEditModeEnabled ? 'xl' : undefined}>
{widget.properties.items.map((item: BookmarkItem, index) => (
<Card
className={classes.autoGridItem}
key={index}
px="xl"
component="a"
href={item.href}
target={item.openNewTab ? '_blank' : undefined}
withBorder
>
<BookmarkItemContent item={item} />
</Card>
))}
</Box>
);
case 'horizontal':
case 'vertical':
return (
<ScrollArea
offsetScrollbars
type="always"
h="100%"
mr={isEditModeEnabled ? 'xl' : undefined}
>
<Flex
style={{ flexDirection: widget.properties.layout === 'vertical' ? 'column' : 'row' }}
gap="md"
<Stack h="100%" spacing={0}>
<Title size="h4" px="0.25rem">{widget.properties.name}</Title>
<Box
className={classes.grid}
mr={isEditModeEnabled && widget.properties.name === "" ? 'xl' : undefined}
h="100%"
>
{widget.properties.items.map((item: BookmarkItem, index) => (
<Card
className={classes.autoGridItem}
key={index}
w={widget.properties.layout === 'vertical' ? '100%' : undefined}
px="xl"
radius="md"
component="a"
href={item.href}
target={item.openNewTab ? '_blank' : undefined}
withBorder
bg={colorScheme === 'dark' ? colors.dark[5].concat('80') : colors.blue[0].concat('80')}
sx={{
'&:hover': { backgroundColor: fn.primaryColor().concat('40'), }, //'40' = 25% opacity
flex:'1 1 auto',
}}
display="flex"
>
<BookmarkItemContent item={item} />
</Card>
))}
</Flex>
</ScrollArea>
</Box>
</Stack>
);
case 'horizontal':
case 'vertical':
return (
<Stack h="100%" spacing={0}>
<Title size="h4" px="0.25rem">
{widget.properties.name}
</Title>
<ScrollArea
scrollbarSize={8}
type="auto"
h="100%"
offsetScrollbars
mr={isEditModeEnabled && widget.properties.name === ""? 'xl' : undefined}
styles={{
viewport:{
//mantine being mantine again... this might break. Needed for taking 100% of widget space
'& div[style="min-width: 100%; display: table;"]':{
height:'100%',
},
},
}}
>
<Flex
direction={ widget.properties.layout === 'vertical' ? 'column' : 'row' }
gap="0"
h="100%"
>
{widget.properties.items.map((item: BookmarkItem, index) => (
<>
<Divider
m="1px"
orientation={ widget.properties.layout !== 'vertical' ? 'vertical' : 'horizontal' } //Mantine doesn't let me refactor this, I tried
hidden={!(index > 0)}
/>
<Card
key={index}
px="md"
py="1px"
component="a"
href={item.href}
target={item.openNewTab ? '_blank' : undefined}
radius="md"
bg="transparent"
sx={{
'&:hover': { backgroundColor: fn.primaryColor().concat('40'),}, //'40' = 25% opacity
flex:'1 1 auto'
}}
display="flex"
>
<BookmarkItemContent item={item}/>
</Card>
</>
))}
</Flex>
</ScrollArea>
</Stack>
);
default:
return null;
}
}
const BookmarkItemContent = ({ item }: { item: BookmarkItem }) => (
<Group>
<Image src={item.iconUrl} width={30} height={30} fit="contain" withPlaceholder />
const BookmarkItemContent = ({ item }: { item: BookmarkItem }) => {
const { colorScheme } = useMantineTheme();
return (
<Group spacing="0rem 1rem">
<Image
hidden={item.hideIcon}
src={item.iconUrl}
width={47}
height={47}
fit="contain"
withPlaceholder />
<Stack spacing={0}>
<Text>{item.name}</Text>
<Text color="dimmed" size="sm">
<Text size="md">{item.name}</Text>
<Text
color={colorScheme === 'dark' ? "gray.6" : "gray.7"}
size="sm"
hidden={item.hideHostname}
>
{new URL(item.href).hostname}
</Text>
</Stack>
</Group>
);
)};
const useStyles = createStyles(() => ({
grid: {
display: 'grid',
gap: 20,
gap: 10,
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
},
autoGridItem: {

View File

@@ -0,0 +1,90 @@
import { useConfigContext } from '~/config/provider';
import { RouterInputs, api } from '~/utils/api';
import { UsenetHistoryRequestParams, UsenetInfoRequestParams, UsenetPauseRequestParams, UsenetQueueRequestParams, UsenetResumeRequestParams } from '../useNet/types';
const POLLING_INTERVAL = 2000;
export const useGetUsenetInfo = ({ appId }: UsenetInfoRequestParams) => {
const { name: configName } = useConfigContext();
return api.usenet.info.useQuery(
{
appId,
configName: configName!,
},
{
refetchInterval: POLLING_INTERVAL,
keepPreviousData: true,
retry: 2,
enabled: !!appId,
}
);
};
export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) => {
const { name: configName } = useConfigContext();
return api.usenet.queue.useQuery(
{
configName: configName!,
...params,
},
{
refetchInterval: POLLING_INTERVAL,
keepPreviousData: true,
retry: 2,
}
);
};
export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) => {
const { name: configName } = useConfigContext();
return api.usenet.history.useQuery(
{
configName: configName!,
...params,
},
{
refetchInterval: POLLING_INTERVAL,
keepPreviousData: true,
retry: 2,
}
);
};
export const usePauseUsenetQueueMutation = (params: UsenetPauseRequestParams) => {
const { name: configName } = useConfigContext();
const { mutateAsync } = api.usenet.pause.useMutation();
const utils = api.useContext();
return async (variables: Omit<RouterInputs['usenet']['pause'], 'configName'>) => {
await mutateAsync(
{
configName: configName!,
...variables,
},
{
onSettled() {
utils.usenet.info.invalidate({ appId: params.appId });
},
}
);
};
};
export const useResumeUsenetQueueMutation = (params: UsenetResumeRequestParams) => {
const { name: configName } = useConfigContext();
const { mutateAsync } = api.usenet.resume.useMutation();
const utils = api.useContext();
return async (variables: Omit<RouterInputs['usenet']['resume'], 'configName'>) => {
await mutateAsync(
{
configName: configName!,
...variables,
},
{
onSettled() {
utils.usenet.info.invalidate({ appId: params.appId });
},
}
);
};
};

View File

@@ -18,7 +18,7 @@ import { useEffect } from 'react';
import { AppAvatar } from '../../components/AppAvatar';
import { useConfigContext } from '../../config/provider';
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
import { useGetDownloadClientsQueue } from './useGetNetworkSpeed';
import { useColorTheme } from '../../tools/color';
import { humanFileSize } from '../../tools/humanFileSize';
import {

View File

@@ -0,0 +1,14 @@
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
export const useGetDownloadClientsQueue = () => {
const { name: configName } = useConfigContext();
return api.download.get.useQuery(
{
configName: configName!,
},
{
refetchInterval: 3000,
}
);
};

View File

@@ -15,7 +15,7 @@ import { useTranslation } from 'next-i18next';
import { AppAvatar } from '../../components/AppAvatar';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
import { useConfigContext } from '../../config/provider';
import { useGetMediaServers } from '../../hooks/widgets/media-servers/useGetMediaServers';
import { useGetMediaServers } from './useGetMediaServers';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { TableRow } from './TableRow';

View File

@@ -0,0 +1,20 @@
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
interface GetMediaServersParams {
enabled: boolean;
}
export const useGetMediaServers = ({ enabled }: GetMediaServersParams) => {
const { name: configName } = useConfigContext();
return api.mediaServer.all.useQuery(
{
configName: configName!,
},
{
enabled,
refetchInterval: 10 * 1000,
}
);
};

View File

@@ -19,7 +19,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import { useTranslation } from 'next-i18next';
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
import { AppIntegrationType } from '../../types/app';
import { defineWidget } from '../helper';

View File

@@ -12,7 +12,7 @@ import {
useGetUsenetInfo,
usePauseUsenetQueueMutation,
useResumeUsenetQueueMutation,
} from '../../hooks/widgets/dashDot/api';
} from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize';
import { AppIntegrationType } from '../../types/app';
import { defineWidget } from '../helper';

View File

@@ -18,7 +18,7 @@ import duration from 'dayjs/plugin/duration';
import { useTranslation } from 'next-i18next';
import { FunctionComponent, useState } from 'react';
import { useGetUsenetHistory } from '../../hooks/widgets/dashDot/api';
import { useGetUsenetHistory } from '../dashDot/api';
import { parseDuration } from '../../tools/client/parseDuration';
import { humanFileSize } from '../../tools/humanFileSize';

View File

@@ -21,7 +21,7 @@ import duration from 'dayjs/plugin/duration';
import { useTranslation } from 'next-i18next';
import { FunctionComponent, useState } from 'react';
import { useGetUsenetDownloads } from '../../hooks/widgets/dashDot/api';
import { useGetUsenetDownloads } from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize';
dayjs.extend(duration);

View File

@@ -18,3 +18,45 @@ export interface UsenetHistoryItem {
id: string;
time: number;
}
export interface UsenetHistoryRequestParams {
appId: string;
offset: number;
limit: number;
}
export interface UsenetHistoryResponse {
items: UsenetHistoryItem[];
total: number;
}
export interface UsenetInfoRequestParams {
appId: string;
}
export interface UsenetInfoResponse {
paused: boolean;
sizeLeft: number;
speed: number;
eta: number;
}
export interface UsenetPauseRequestParams {
appId: string;
}
export interface UsenetQueueRequestParams {
appId: string;
offset: number;
limit: number;
}
export interface UsenetQueueResponse {
items: UsenetQueueItem[];
total: number;
}
export interface UsenetResumeRequestParams {
appId: string;
nzbId?: string;
}