Merge branch 'dev' of https://github.com/ajnart/homarr into widget-option-tooltips
This commit is contained in:
@@ -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: {
|
||||
|
||||
90
src/widgets/dashDot/api.ts
Normal file
90
src/widgets/dashDot/api.ts
Normal 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 });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
14
src/widgets/download-speed/useGetNetworkSpeed.tsx
Normal file
14
src/widgets/download-speed/useGetNetworkSpeed.tsx
Normal 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,
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
20
src/widgets/media-server/useGetMediaServers.tsx
Normal file
20
src/widgets/media-server/useGetMediaServers.tsx
Normal 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,
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user