Add database board on overview, Migrate all widgets to new widget system, Improve gridstack, Migrate sidebar to new system

This commit is contained in:
Meier Lukas
2023-10-01 14:40:18 +02:00
parent c5becb76f8
commit 1b4070c9ce
40 changed files with 675 additions and 605 deletions

View File

@@ -19,6 +19,7 @@
},
"badges": {
"fileSystem": "File system",
"database": "Database",
"default": "Default"
}
},

View File

@@ -0,0 +1,109 @@
import { useCallback } from 'react';
import { api } from '~/utils/api';
import { useRequiredBoard } from './context';
type MoveAndResizeItem = {
itemId: string;
x: number;
y: number;
width: number;
height: number;
};
type MoveItemToSection = {
itemId: string;
sectionId: string;
x: number;
y: number;
width: number;
height: number;
};
export const useItemActions = () => {
const board = useRequiredBoard();
const utils = api.useContext();
const moveAndResizeItem = useCallback(
({ itemId, ...positionProps }: MoveAndResizeItem) => {
utils.boards.byName.setData({ boardName: board.name }, (prev) => {
if (!prev) return prev;
return {
...prev,
sections: prev.sections.map((section) => {
// Return same section if item is not in it
if (!section.items.some((item) => item.id === itemId)) return section;
return {
...section,
items: section.items.map((item) => {
// Return same item if item is not the one we're moving
if (item.id !== itemId) return item;
return {
...item,
...positionProps,
};
}),
};
}),
};
});
},
[board.name, utils]
);
const moveItemToSection = useCallback(
({ itemId, sectionId, ...positionProps }: MoveItemToSection) => {
utils.boards.byName.setData({ boardName: board.name }, (prev) => {
if (!prev) return prev;
const currentSection = prev.sections.find((section) =>
section.items.some((item) => item.id === itemId)
);
// If item is in the same section (on initial loading) don't do anything
if (!currentSection || currentSection.id === sectionId) return prev;
let currentItem = currentSection?.items.find((item) => item.id === itemId);
if (!currentItem) return prev;
return {
...prev,
sections: prev.sections.map((section) => {
// Return sections without item if not section where it is moved to
if (section.id !== sectionId)
return {
...section,
items: section.items.filter((item) => item.id !== itemId),
};
// Return section and add item to it
return {
...section,
items: section.items.concat({
...currentItem!,
...positionProps,
}),
};
}),
};
});
},
[board.name, utils]
);
return {
moveAndResizeItem,
moveItemToSection,
};
};
/*
- Add category (on top, below, above)
- Rename category
- Move category (down & up)
- Remove category
- Add widget
- Edit widget
- Remove widget
- Add app
- Edit app
- Remove app
*/

View File

@@ -1,27 +1,32 @@
import { ActionIcon, Space, createStyles } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
import { useConfigContext } from '~/config/provider';
import { SidebarSection, useRequiredBoard } from '~/components/Board/context';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { MobileRibbonSidebarDrawer } from './MobileRibbonSidebarDrawer';
export const MobileRibbons = () => {
const { classes, cx } = useStyles();
const { config } = useConfigContext();
const board = useRequiredBoard();
const [openedRight, rightSidebar] = useDisclosure(false);
const [openedLeft, leftSidebar] = useDisclosure(false);
const screenLargerThanMd = useScreenLargerThan('md');
if (screenLargerThanMd || !config) {
if (screenLargerThanMd) {
return <></>;
}
const layoutSettings = config.settings.customization.layout;
const leftSection = board.sections.find(
(x): x is SidebarSection => x.type === 'sidebar' && x.position === 'left'
)!;
const rightSection = board.sections.find(
(x): x is SidebarSection => x.type === 'sidebar' && x.position === 'right'
)!;
return (
<div className={classes.root}>
{layoutSettings.enabledLeftSidebar ? (
{board.isLeftSidebarVisible && leftSection ? (
<>
<ActionIcon
onClick={leftSidebar.open}
@@ -33,14 +38,14 @@ export const MobileRibbons = () => {
<MobileRibbonSidebarDrawer
onClose={leftSidebar.close}
opened={openedLeft}
location="left"
section={leftSection}
/>
</>
) : (
<Space />
)}
{layoutSettings.enabledRightSidebar ? (
{board.isRightSidebarVisible && rightSection ? (
<>
<ActionIcon
onClick={rightSidebar.open}
@@ -52,7 +57,7 @@ export const MobileRibbons = () => {
<MobileRibbonSidebarDrawer
onClose={rightSidebar.close}
opened={openedRight}
location="right"
section={rightSection}
/>
</>
) : null}

View File

@@ -1,24 +1,25 @@
import { Drawer, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { SidebarSection } from '~/components/Board/context';
import { DashboardSidebar } from '../../Wrappers/Sidebar/Sidebar';
interface MobileRibbonSidebarDrawerProps {
onClose: () => void;
opened: boolean;
location: 'left' | 'right';
section: SidebarSection;
}
export const MobileRibbonSidebarDrawer = ({
location,
section,
...props
}: MobileRibbonSidebarDrawerProps) => {
const { t } = useTranslation('layout/mobile/drawer');
return (
<Drawer
padding={10}
position={location}
title={<Title order={4}>{t('title', { position: location })}</Title>}
position={section.position}
title={<Title order={4}>{t('title', { position: section.position })}</Title>}
style={{
display: 'flex',
justifyContent: 'center',
@@ -28,10 +29,10 @@ export const MobileRibbonSidebarDrawer = ({
width: '100%',
},
}}
transitionProps={{ transition: `slide-${location === 'right' ? 'left' : 'right'}` }}
transitionProps={{ transition: `slide-${section.position === 'right' ? 'left' : 'right'}` }}
{...props}
>
<DashboardSidebar location={location} isGridstackReady />
<DashboardSidebar section={section} isGridstackReady />
</Drawer>
);
};

View File

@@ -1,8 +1,8 @@
import { SelectItem } from '@mantine/core';
import { ContextModalProps, closeModal } from '@mantine/modals';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import widgets from '../../../../widgets';
import { WidgetChangePositionModalInnerProps } from '../../Tiles/Widgets/WidgetsMenu';
import { useGridstackStore, useWrapperColumnCount } from '../../Wrappers/gridstack/store';
@@ -64,10 +64,10 @@ export const ChangeWidgetPositionModal = ({
onCancel={handleCancel}
heightData={heightData}
widthData={widthData}
initialX={innerProps.widget.shape[shapeSize]?.location.x}
initialY={innerProps.widget.shape[shapeSize]?.location.y}
initialWidth={innerProps.widget.shape[shapeSize]?.size.width}
initialHeight={innerProps.widget.shape[shapeSize]?.size.height}
initialX={innerProps.widget.x}
initialY={innerProps.widget.y}
initialWidth={innerProps.widget.width}
initialHeight={innerProps.widget.height}
/>
);
};

View File

@@ -2,7 +2,6 @@ import { Button, Card, Center, Grid, Stack, Text } from '@mantine/core';
import { Icon } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import React from 'react';
import { useStyles } from './styles';

View File

@@ -1,11 +1,13 @@
import { AppItem } from '~/components/Board/context';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions';
import { AppType } from '~/types/app';
import { GenericTileMenu } from '../GenericTileMenu';
interface TileMenuProps {
app: AppType;
app: AppItem;
}
export const AppMenu = ({ app }: TileMenuProps) => {
@@ -13,7 +15,7 @@ export const AppMenu = ({ app }: TileMenuProps) => {
const { updateConfig } = useConfigStore();
const handleClickEdit = () => {
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
openContextModalGeneric<{ app: AppItem; allowAppNamePropagation: boolean }>({
modal: 'editApp',
size: 'xl',
innerProps: {

View File

@@ -6,7 +6,6 @@ import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { AppItem } from '~/components/Board/context';
import { useConfigContext } from '~/config/provider';
import { AppType } from '~/types/app';
import { RouterOutputs, api } from '~/utils/api';
interface AppPingProps {

View File

@@ -13,7 +13,7 @@ import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
export type WidgetChangePositionModalInnerProps = {
widgetId: string;
widgetType: string;
widget: IWidget<string, any>;
widget: WidgetItem;
wrapperColumnCount: number;
};

View File

@@ -1,43 +1,71 @@
import { Group, Stack } from '@mantine/core';
import { Box, Group, LoadingOverlay, Stack } from '@mantine/core';
import { useEffect, useRef } from 'react';
import { CategorySection, EmptySection, useRequiredBoard } from '~/components/Board/context';
import {
CategorySection,
EmptySection,
SidebarSection,
useRequiredBoard,
} from '~/components/Board/context';
import { useResize } from '~/hooks/use-resize';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { DashboardCategory } from '../Wrappers/Category/Category';
import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar';
import { DashboardWrapper } from '../Wrappers/Wrapper/Wrapper';
import { useGridstackStore } from '../Wrappers/gridstack/store';
export const BoardView = () => {
const sections = useStackedSections();
const stackedSections = useStackedSections();
const sidebarsVisible = useSidebarVisibility();
const { isReady, mainAreaRef } = usePrepareGridstack();
const leftSidebarSection = useSidebarSection('left');
const rightSidebarSection = useSidebarSection('right');
return (
<Group align="top" h="100%" spacing="xs">
{sidebarsVisible.left ? (
<DashboardSidebar location="left" isGridstackReady={isReady} />
) : null}
<Box h="100%" pos="relative">
<LoadingOverlay
visible={!isReady}
transitionDuration={500}
loaderProps={{ size: 'lg', variant: 'bars' }}
/>
<Group
align="top"
h="100%"
spacing="xs"
style={{ visibility: isReady ? 'visible' : 'hidden' }}
>
{sidebarsVisible.left && leftSidebarSection ? (
<DashboardSidebar section={leftSidebarSection} isGridstackReady={isReady} />
) : null}
<Stack ref={mainAreaRef} mx={-10} style={{ flexGrow: 1 }}>
{isReady &&
sections.map((item) =>
<Stack ref={mainAreaRef} mx={-10} style={{ flexGrow: 1 }}>
{stackedSections.map((item) =>
item.type === 'category' ? (
<span>{item.name}</span>
<DashboardCategory key={item.id} section={item} />
) : (
<DashboardWrapper key={item.id} section={item} />
)
)}
</Stack>
</Stack>
{sidebarsVisible.right ? (
<DashboardSidebar location="right" isGridstackReady={isReady} />
) : null}
</Group>
{sidebarsVisible.right && rightSidebarSection ? (
<DashboardSidebar section={rightSidebarSection} isGridstackReady={isReady} />
) : null}
</Group>
</Box>
);
};
// <DashboardCategory key={item.id} category={item as unknown as CategoryType} />
// <DashboardWrapper key={item.id} wrapper={item as WrapperType} />
/*
{sidebarsVisible.left ? (
<DashboardSidebar location="left" isGridstackReady={isReady} />
) : null}
{sidebarsVisible.right ? (
<DashboardSidebar location="right" isGridstackReady={isReady} />
) : null}
*/
const usePrepareGridstack = () => {
const mainAreaRef = useRef<HTMLDivElement>(null);
@@ -51,7 +79,7 @@ const usePrepareGridstack = () => {
}, [width]);
return {
isReady: Boolean(mainAreaWidth),
isReady: !!mainAreaWidth,
mainAreaRef,
};
};
@@ -75,3 +103,10 @@ const useStackedSections = () => {
(s): s is CategorySection | EmptySection => s.type === 'category' || s.type === 'empty'
);
};
const useSidebarSection = (position: 'left' | 'right') => {
const board = useRequiredBoard();
return board.sections.find(
(s): s is SidebarSection => s.type === 'sidebar' && s.position === position
);
};

View File

@@ -1,16 +1,17 @@
import { Card } from '@mantine/core';
import { RefObject } from 'react';
import { SidebarSection } from '~/components/Board/context';
import { useCardStyles } from '../../../layout/Common/useCardStyles';
import { WrapperContent } from '../WrapperContent';
import { useGridstack } from '../gridstack/use-gridstack';
interface DashboardSidebarProps extends DashboardSidebarInnerProps {
location: 'right' | 'left';
section: SidebarSection;
isGridstackReady: boolean;
}
export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSidebarProps) => {
export const DashboardSidebar = ({ section, isGridstackReady }: DashboardSidebarProps) => {
const {
cx,
classes: { card: cardClass },
@@ -18,18 +19,18 @@ export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSideba
return (
<Card p={0} m={0} radius="lg" className={cardClass} withBorder>
{isGridstackReady && <SidebarInner location={location} />}
{isGridstackReady && <SidebarInner section={section} />}
</Card>
);
};
interface DashboardSidebarInnerProps {
location: 'right' | 'left';
section: SidebarSection;
}
// Is Required because of the gridstack main area width.
const SidebarInner = ({ location }: DashboardSidebarInnerProps) => {
const { refs, apps, widgets } = useGridstack('sidebar', location);
const SidebarInner = ({ section }: DashboardSidebarInnerProps) => {
const { refs } = useGridstack({ section });
const minRow = useMinRowForFullHeight(refs.wrapper);
@@ -43,11 +44,11 @@ const SidebarInner = ({ location }: DashboardSidebarInnerProps) => {
height: '100%',
width: '100%',
}}
data-sidebar={location}
data-sidebar={section.id}
// eslint-disable-next-line react/no-unknown-property
gs-min-row={minRow}
>
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
<WrapperContent items={section.items} refs={refs} />
</div>
);
};

View File

@@ -1,33 +1,40 @@
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject } from 'react';
import { Item } from '~/components/Board/context';
import { Item, Section } from '~/components/Board/context';
export const initializeGridstack = (
areaType: 'wrapper' | 'category' | 'sidebar',
wrapperRef: RefObject<HTMLDivElement>,
gridRef: MutableRefObject<GridStack | undefined>,
itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>,
areaId: string,
items: Item[],
isEditMode: boolean,
wrapperColumnCount: number,
shapeSize: 'sm' | 'md' | 'lg',
tilesWithUnknownLocation: TileWithUnknownLocation[],
type InitializeGridstackProps = {
section: Section;
refs: {
wrapper: RefObject<HTMLDivElement>;
items: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>;
gridstack: MutableRefObject<GridStack | undefined>;
};
isEditMode: boolean;
sectionColumnCount: number;
events: {
onChange: (changedNode: GridStackNode) => void;
onAdd: (addedNode: GridStackNode) => void;
}
) => {
if (!wrapperRef.current) return;
};
};
export const initializeGridstack = ({
section,
refs,
isEditMode,
sectionColumnCount,
events,
}: InitializeGridstackProps) => {
if (!refs.wrapper.current) return;
// calculates the currently available count of columns
const columnCount = areaType === 'sidebar' ? 2 : wrapperColumnCount;
const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 128);
const columnCount = section.type === 'sidebar' ? 2 : sectionColumnCount;
const minRow =
section.type !== 'sidebar' ? 1 : Math.floor(refs.wrapper.current.offsetHeight / 128);
// initialize gridstack
const newGrid = gridRef;
const newGrid = refs.gridstack;
newGrid.current = GridStack.init(
{
column: columnCount,
margin: areaType === 'sidebar' ? 5 : 10,
margin: section.type === 'sidebar' ? 5 : 10,
cellHeight: 128,
float: true,
alwaysShowResizeHandle: 'mobile',
@@ -38,7 +45,7 @@ export const initializeGridstack = (
animate: false,
},
// selector of the gridstack item (it's eather category or wrapper)
`.grid-stack-${areaType}[data-${areaType}='${areaId}']`
`.grid-stack-${section.type}[data-${section.type}='${section.id}']`
);
const grid = newGrid.current;
if (!grid) return;
@@ -63,17 +70,10 @@ export const initializeGridstack = (
grid.batchUpdate();
grid.removeAll(false);
items.forEach((item) => {
const ref = itemRefs.current[item.id]?.current;
section.items.forEach((item) => {
const ref = refs.items.current[item.id]?.current;
setAttributesFromShape(ref, item);
ref && grid.makeWidget(ref as HTMLDivElement);
/*if (!item && ref) {
const gridItemElement = ref as GridItemHTMLElement;
if (gridItemElement.gridstackNode) {
const { x, y, w, h } = gridItemElement.gridstackNode;
tilesWithUnknownLocation.push({ x, y, w, h, type: 'app', id: item.id });
}
}*/
});
grid.batchUpdate(false);
};

View File

@@ -1,14 +1,18 @@
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
import { MutableRefObject, RefObject, createRef, useEffect, useMemo, useRef } from 'react';
import { Item, Section } from '~/components/Board/context';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { AppType } from '~/types/app';
import { AreaType } from '~/types/area';
import { IWidget } from '~/widgets/widgets';
import {
MutableRefObject,
RefObject,
createRef,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { Section } from '~/components/Board/context';
import { useItemActions } from '~/components/Board/item-actions';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { TileWithUnknownLocation, initializeGridstack } from './init-gridstack';
import { initializeGridstack } from './init-gridstack';
import { useGridstackStore, useWrapperColumnCount } from './store';
interface UseGristackReturnType {
@@ -25,20 +29,22 @@ type UseGridstackProps = {
export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnType => {
const isEditMode = useEditModeStore((x) => x.enabled);
const updateConfig = useConfigStore((x) => x.updateConfig);
const { moveAndResizeItem, moveItemToSection } = useItemActions();
// define reference for wrapper - is used to calculate the width of the wrapper
const wrapperRef = useRef<HTMLDivElement>(null);
// references to the diffrent items contained in the gridstack
const itemRefs = useRef<Record<string, RefObject<HTMLDivElement>>>({});
// reference of the gridstack object for modifications after initialization
const gridRef = useRef<GridStack>();
const wrapperColumnCount = useWrapperColumnCount();
const shapeSize = useGridstackStore((x) => x.currentShapeSize);
const sectionColumnCount = useWrapperColumnCount();
const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth);
// width of the wrapper (updating on page resize)
const root: HTMLHtmlElement = useMemo(() => document.querySelector(':root')!, []);
const root = useMemo(() => {
if (typeof document === 'undefined') return;
return document.querySelector(':root')! as HTMLHtmlElement;
}, [typeof document]);
if (!mainAreaWidth || !shapeSize || !wrapperColumnCount) {
if (/*!mainAreaWidth ||*/ !sectionColumnCount) {
throw new Error('UseGridstack should not be executed before mainAreaWidth has been set!');
}
@@ -52,239 +58,81 @@ export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnT
}
useEffect(() => {
if (section.type === 'sidebar') return;
const widgetWidth = mainAreaWidth / wrapperColumnCount;
if (section.type === 'sidebar' || !mainAreaWidth) return;
const widgetWidth = mainAreaWidth / sectionColumnCount;
// widget width is used to define sizes of gridstack items within global.scss
root.style.setProperty('--gridstack-widget-width', widgetWidth.toString());
root?.style.setProperty('--gridstack-widget-width', widgetWidth.toString());
gridRef.current?.cellHeight(widgetWidth);
}, [mainAreaWidth, wrapperColumnCount, gridRef.current]);
}, [mainAreaWidth, sectionColumnCount, gridRef.current]);
useEffect(() => {
// column count is used to define count of columns of gridstack within global.scss
root.style.setProperty('--gridstack-column-count', wrapperColumnCount.toString());
}, [wrapperColumnCount]);
root?.style.setProperty('--gridstack-column-count', sectionColumnCount.toString());
}, [sectionColumnCount]);
const configName = 'default';
const onChange = isEditMode
? (changedNode: GridStackNode) => {
if (!configName) return;
const onChange = useCallback(
(changedNode: GridStackNode) => {
if (!isEditMode) return;
const itemType = changedNode.el?.getAttribute('data-type');
const itemId = changedNode.el?.getAttribute('data-id');
if (!itemType || !itemId) return;
const itemType = changedNode.el?.getAttribute('data-type');
const itemId = changedNode.el?.getAttribute('data-id');
if (!itemType || !itemId) return;
// Updates the config and defines the new position of the item
updateConfig(configName, (previous) => {
const currentItem =
itemType === 'app'
? previous.apps.find((x) => x.id === itemId)
: previous.widgets.find((x) => x.id === itemId);
if (!currentItem) return previous;
// Updates the config and defines the new position of the item
moveAndResizeItem({
itemId,
x: changedNode.x!,
y: changedNode.y!,
width: changedNode.w!,
height: changedNode.h!,
});
},
[isEditMode]
);
currentItem.shape[shapeSize] = {
location: {
x: changedNode.x!,
y: changedNode.y!,
},
size: {
width: changedNode.w!,
height: changedNode.h!,
},
};
const onAdd = useCallback(
(addedNode: GridStackNode) => {
if (!isEditMode) return;
if (itemType === 'app') {
return {
...previous,
apps: [
...previous.apps.filter((x) => x.id !== itemId),
{ ...(currentItem as AppType) },
],
};
}
const itemType = addedNode.el?.getAttribute('data-type');
const itemId = addedNode.el?.getAttribute('data-id');
if (!itemType || !itemId) return;
return {
...previous,
widgets: [
...previous.widgets.filter((x) => x.id !== itemId),
{ ...(currentItem as IWidget<string, any>) },
],
};
});
}
: () => {};
const onAdd = isEditMode
? (addedNode: GridStackNode) => {
if (!configName) return;
const itemType = addedNode.el?.getAttribute('data-type');
const itemId = addedNode.el?.getAttribute('data-id');
if (!itemType || !itemId) return;
// Updates the config and defines the new position and wrapper of the item
updateConfig(
configName,
(previous) => {
const currentItem =
itemType === 'app'
? previous.apps.find((x) => x.id === itemId)
: previous.widgets.find((x) => x.id === itemId);
if (!currentItem) return previous;
if (section.type === 'sidebar') {
currentItem.area = {
type: section.type,
properties: {
location: section.position,
},
};
} else {
currentItem.area = {
type: section.type as any,
properties: {
id: section.id,
},
};
}
currentItem.shape[shapeSize] = {
location: {
x: addedNode.x!,
y: addedNode.y!,
},
size: {
width: addedNode.w!,
height: addedNode.h!,
},
};
if (itemType === 'app') {
return {
...previous,
apps: [
...previous.apps.filter((x) => x.id !== itemId),
{ ...(currentItem as AppType) },
],
};
}
return {
...previous,
widgets: [
...previous.widgets.filter((x) => x.id !== itemId),
{ ...(currentItem as IWidget<string, any>) },
],
};
},
(prev, curr) => {
const isApp = itemType === 'app';
if (isApp) {
const currItem = curr.apps.find((x) => x.id === itemId);
const prevItem = prev.apps.find((x) => x.id === itemId);
if (!currItem || !prevItem) return false;
return (
currItem.area.type !== prevItem.area.type ||
Object.entries(currItem.area.properties).some(
([key, value]) =>
prevItem.area.properties[key as keyof AreaType['properties']] !== value
)
);
}
const currItem = curr.widgets.find((x) => x.id === itemId);
const prevItem = prev.widgets.find((x) => x.id === itemId);
if (!currItem || !prevItem) return false;
return (
currItem.area.type !== prevItem.area.type ||
Object.entries(currItem.area.properties).some(
([key, value]) =>
prevItem.area.properties[key as keyof AreaType['properties']] !== value
)
);
}
);
}
: () => {};
moveItemToSection({
itemId,
sectionId: section.id,
x: addedNode.x!,
y: addedNode.y!,
width: addedNode.w!,
height: addedNode.h!,
});
},
[isEditMode]
);
// initialize the gridstack
useEffect(() => {
const removeEventHandlers = () => {
initializeGridstack({
events: {
onChange,
onAdd,
},
isEditMode,
section,
refs: {
items: itemRefs,
wrapper: wrapperRef,
gridstack: gridRef,
},
sectionColumnCount,
});
// Remove event listeners on unmount
return () => {
gridRef.current?.off('change');
gridRef.current?.off('added');
};
const tilesWithUnknownLocation: TileWithUnknownLocation[] = [];
initializeGridstack(
section.type as any,
wrapperRef,
gridRef,
itemRefs,
section.id,
items,
isEditMode,
wrapperColumnCount,
shapeSize,
tilesWithUnknownLocation,
{
onChange,
onAdd,
}
);
if (!configName) return removeEventHandlers;
updateConfig(configName, (prev) => ({
...prev,
apps: prev.apps.map((app) => {
const currentUnknownLocation = tilesWithUnknownLocation.find(
(x) => x.type === 'app' && x.id === app.id
);
if (!currentUnknownLocation) return app;
return {
...app,
shape: {
...app.shape,
[shapeSize]: {
location: {
x: currentUnknownLocation.x,
y: currentUnknownLocation.y,
},
size: {
width: currentUnknownLocation.w,
height: currentUnknownLocation.h,
},
},
},
};
}),
widgets: prev.widgets.map((widget) => {
const currentUnknownLocation = tilesWithUnknownLocation.find(
(x) => x.type === 'widget' && x.id === widget.id
);
if (!currentUnknownLocation) return widget;
return {
...widget,
shape: {
...widget.shape,
[shapeSize]: {
location: {
x: currentUnknownLocation.x,
y: currentUnknownLocation.y,
},
size: {
width: currentUnknownLocation.w,
height: currentUnknownLocation.h,
},
},
},
};
}),
}));
return removeEventHandlers;
}, [items, wrapperRef.current, wrapperColumnCount]);
}, [items, wrapperRef.current, sectionColumnCount]);
return {
refs: {

View File

@@ -96,7 +96,7 @@ const ToggleEditModeButton = () => {
const { enabled, toggleEditMode } = useEditModeStore();
const board = useRequiredBoard();
const { name } = board;
const { mutateAsync: saveConfig } = api.config.save.useMutation();
//const { mutateAsync: saveConfig } = api.config.save.useMutation();
const namedWrapperColumnCount = useNamedWrapperColumnCount();
const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']);
const translatedSize =
@@ -119,7 +119,7 @@ const ToggleEditModeButton = () => {
const save = async () => {
toggleEditMode();
if (!board || !name) return;
await saveConfig({ name, config: {} as any });
//await saveConfig({ name, config: {} as any });
Consola.log('Saved config to server', name);
hideNotification(editModeNotificationId);
};

View File

@@ -1,18 +1,22 @@
import { ColorScheme as MantineColorScheme, MantineProvider, MantineTheme } from '@mantine/core';
import {
type ColorScheme as MantineColorScheme,
MantineProvider,
type MantineTheme,
} from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import Consola from 'consola';
import { consola } from 'consola';
import { getCookie, setCookie } from 'cookies-next';
import dayjs from 'dayjs';
import locale from 'dayjs/plugin/localeData';
import utc from 'dayjs/plugin/utc';
import 'flag-icons/css/flag-icons.min.css';
import { GetServerSidePropsContext } from 'next';
import { Session } from 'next-auth';
import { type GetServerSidePropsContext } from 'next';
import { type Session } from 'next-auth';
import { SessionProvider, getSession } from 'next-auth/react';
import { appWithTranslation } from 'next-i18next';
import { AppProps } from 'next/app';
import { type AppProps } from 'next/app';
import { useEffect, useState } from 'react';
import 'video.js/dist/video-js.css';
import { CommonHead } from '~/components/layout/Meta/CommonHead';
@@ -23,7 +27,7 @@ import { modals } from '~/modals';
import { ColorTheme } from '~/tools/color';
import { getLanguageByCode } from '~/tools/language';
import {
ServerSidePackageAttributesType,
type ServerSidePackageAttributesType,
getServiceSidePackageAttributes,
} from '~/tools/server/getPackageVersion';
import { theme } from '~/tools/server/theme/theme';
@@ -140,7 +144,7 @@ function App(
App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
if (env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME !== 'light') {
Consola.debug(
consola.debug(
`Overriding the default color scheme with ${env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME}`
);
}

View File

@@ -15,6 +15,7 @@ import { useListState } from '@mantine/hooks';
import {
IconBox,
IconCategory,
IconDatabase,
IconDeviceFloppy,
IconDotsVertical,
IconFolderFilled,
@@ -23,7 +24,7 @@ import {
IconStarFilled,
IconTrash,
} from '@tabler/icons-react';
import { GetServerSideProps } from 'next';
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import Head from 'next/head';
@@ -31,6 +32,7 @@ import Link from 'next/link';
import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { createTrpcServersideHelpers } from '~/server/api/helper';
import { getServerAuthSession } from '~/server/auth';
import { sleep } from '~/tools/client/time';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
@@ -38,10 +40,12 @@ import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api';
const BoardsPage = () => {
const BoardsPage = ({ initialBoards }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const context = api.useContext();
const { data: sessionData } = useSession();
const { data } = api.boards.all.useQuery();
const { data } = api.boards.all.useQuery(undefined, {
initialData: initialBoards,
});
const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({
onSettled: () => {
void context.boards.invalidate();
@@ -91,13 +95,19 @@ const BoardsPage = () => {
{board.name}
</Text>
<Group spacing="xs" noWrap>
<Badge
leftSection={<IconFolderFilled size=".7rem" />}
color="pink"
variant="light"
>
{t('cards.badges.fileSystem')}
</Badge>
{board.type === 'file' ? (
<Badge
leftSection={<IconFolderFilled size=".7rem" />}
color="pink"
variant="light"
>
{t('cards.badges.fileSystem')}
</Badge>
) : (
<Badge leftSection={<IconDatabase size=".7rem" />} color="blue" variant="light">
{t('cards.badges.database')}
</Badge>
)}
{board.isDefaultForUser && (
<Badge
leftSection={<IconStarFilled size=".7rem" />}
@@ -209,6 +219,9 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
return result;
}
const helpers = await createTrpcServersideHelpers(ctx);
const initialBoards = await helpers.boards.all.fetch();
const translations = await getServerSideTranslations(
manageNamespaces,
ctx.locale,
@@ -217,6 +230,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
);
return {
props: {
initialBoards,
...translations,
},
};

View File

@@ -3,61 +3,70 @@ import axios, { AxiosError } from 'axios';
import Consola from 'consola';
import https from 'https';
import { z } from 'zod';
import { isStatusOk } from '~/components/Dashboard/Tiles/Apps/AppPing';
import { getConfig } from '~/tools/config/getConfig';
import { AppType } from '~/types/app';
import { createTRPCRouter, publicProcedure } from '../trpc';
export const appRouter = createTRPCRouter({
ping: publicProcedure.input(z.object({
id: z.string(),
configName: z.string()
})).query(async ({ input }) => {
const agent = new https.Agent({ rejectUnauthorized: false });
const config = getConfig(input.configName);
const app = config.apps.find((app) => app.id === input.id);
ping: publicProcedure
.input(
z.object({
id: z.string(),
configName: z.string(),
})
)
.query(async ({ input }) => {
const agent = new https.Agent({ rejectUnauthorized: false });
const config = getConfig(input.configName);
const app = config.apps.find((app) => app.id === input.id);
if (!app?.url) {
Consola.error(`App ${input} not found`);
throw new TRPCError({
code: 'NOT_FOUND',
cause: input,
message: `App ${input.id} was not found`,
});
}
const res = await axios
.get(app.url, { httpsAgent: agent, timeout: 10000 })
.then((response) => ({
status: response.status,
statusText: response.statusText,
state: isStatusOk(app as AppType, response.status) ? 'online' : 'offline'
}))
.catch((error: AxiosError) => {
if (error.response) {
return {
state: isStatusOk(app as AppType, error.response.status) ? 'online' : 'offline',
status: error.response.status,
statusText: error.response.statusText,
};
}
if (error.code === 'ECONNABORTED') {
Consola.error(`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`);
throw new TRPCError({
code: 'TIMEOUT',
cause: input,
message: `Ping timed out`,
});
}
Consola.error(`Unexpected response: ${error.message}`);
if (!app?.url) {
Consola.error(`App ${input} not found`);
throw new TRPCError({
code: 'UNPROCESSABLE_CONTENT',
code: 'NOT_FOUND',
cause: input,
message: `Unexpected response: ${error.message}`,
message: `App ${input.id} was not found`,
});
});
return res;
}),
}
const res = await axios
.get(app.url, { httpsAgent: agent, timeout: 10000 })
.then((response) => ({
status: response.status,
statusText: response.statusText,
state: app.network.statusCodes?.includes(response.status.toString())
? 'online'
: 'offline',
}))
.catch((error: AxiosError) => {
if (error.response) {
return {
state: app.network.statusCodes?.includes(error.response.status.toString())
? 'online'
: 'offline',
status: error.response.status,
statusText: error.response.statusText,
};
}
if (error.code === 'ECONNABORTED') {
Consola.error(
`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`
);
throw new TRPCError({
code: 'TIMEOUT',
cause: input,
message: `Ping timed out`,
});
}
Consola.error(`Unexpected response: ${error.message}`);
throw new TRPCError({
code: 'UNPROCESSABLE_CONTENT',
cause: input,
message: `Unexpected response: ${error.message}`,
});
});
return res;
}),
});

View File

@@ -28,10 +28,18 @@ import { configNameSchema } from './config';
export const boardRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => {
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
const dbBoards = await db.query.boards.findMany({
columns: {
name: true,
},
with: {
items: true,
},
});
const defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default');
return await Promise.all(
const result = await Promise.all(
files.map(async (file) => {
const name = file.replace('.json', '');
const config = await getFrontendConfig(name);
@@ -44,9 +52,21 @@ export const boardRouter = createTRPCRouter({
countWidgets: config.widgets.length,
countCategories: config.categories.length,
isDefaultForUser: name === defaultBoard,
type: 'file',
};
})
);
return result.concat(
dbBoards.map((x) => ({
name: x.name,
countApps: x.items.filter((x) => x.type === 'app').length,
countWidgets: x.items.filter((x) => x.type === 'widget').length,
countCategories: 0, // TODO: Is different depending on layout
isDefaultForUser: x.name === defaultBoard,
type: 'db',
}))
);
}),
addAppsForContainers: adminProcedure
.input(
@@ -514,7 +534,7 @@ const mapSection = (
return {
...sectionProps,
type,
position: section.position === 0 ? ('left' as const) : ('right' as const),
position: position === 0 ? ('left' as const) : ('right' as const),
items,
};
};

View File

@@ -1,20 +1,28 @@
import Consola from 'consola';
import { removeTrailingSlash } from 'next/dist/shared/lib/router/utils/remove-trailing-slash';
import { z } from 'zod';
import { checkIntegrationsType } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
import { MediaRequestListWidget } from '~/widgets/media-requests/MediaRequestListTile';
import {
MediaRequestListWidget,
MediaRequestListWidgetOptions,
} from '~/widgets/media-requests/MediaRequestListTile';
import {
MediaRequestStatsWidget,
MediaRequestStatsWidgetOptions,
} from '~/widgets/media-requests/MediaRequestStatsTile';
import { MediaRequest, Users } from '~/widgets/media-requests/media-request-types';
import { createTRPCRouter, publicProcedure } from '../trpc';
import { MediaRequestStatsWidget } from '~/widgets/media-requests/MediaRequestStatsTile';
import { removeTrailingSlash } from 'next/dist/shared/lib/router/utils/remove-trailing-slash';
export const mediaRequestsRouter = createTRPCRouter({
allMedia: publicProcedure
.input(
z.object({
configName: z.string(),
widget: z.custom<MediaRequestListWidget>().or(z.custom<MediaRequestStatsWidget>()),
options: z
.custom<MediaRequestListWidgetOptions>()
.or(z.custom<MediaRequestStatsWidgetOptions>()),
})
)
.query(async ({ input }) => {
@@ -33,9 +41,10 @@ export const mediaRequestsRouter = createTRPCRouter({
})
.then(async (response) => {
const body = (await response.json()) as OverseerrResponse;
let appUrl = input.widget.properties.replaceLinksWithExternalHost && app.behaviour.externalUrl?.length > 0
? app.behaviour.externalUrl
: app.url;
let appUrl =
input.options.replaceLinksWithExternalHost && app.behaviour.externalUrl?.length > 0
? app.behaviour.externalUrl
: app.url;
appUrl = removeTrailingSlash(appUrl);
@@ -86,7 +95,9 @@ export const mediaRequestsRouter = createTRPCRouter({
.input(
z.object({
configName: z.string(),
widget: z.custom<MediaRequestListWidget>().or(z.custom<MediaRequestStatsWidget>()),
options: z
.custom<MediaRequestListWidgetOptions>()
.or(z.custom<MediaRequestStatsWidgetOptions>()),
})
)
.query(async ({ input }) => {
@@ -105,7 +116,7 @@ export const mediaRequestsRouter = createTRPCRouter({
})
.then(async (response) => {
const body = (await response.json()) as OverseerrUsers;
const appUrl = input.widget.properties.replaceLinksWithExternalHost
const appUrl = input.options.replaceLinksWithExternalHost
? app.behaviour.externalUrl
: app.url;
@@ -163,7 +174,7 @@ const retrieveDetailsForItem = async (
backdropPath: series.backdropPath,
posterPath: series.backdropPath,
};
};
}
const movieResponse = await fetch(`${baseUrl}/api/v1/movie/${id}`, {
headers,

View File

@@ -16,7 +16,7 @@ export const notebookRouter = createTRPCRouter({
if (process.env.DISABLE_EDIT_MODE?.toLowerCase() === 'true') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: 'Edit is not allowed, because edit mode is disabled'
message: 'Edit is not allowed, because edit mode is disabled',
});
}
@@ -32,14 +32,15 @@ export const notebookRouter = createTRPCRouter({
});
}
widget.properties.content = input.content;
widget.options.content = input.content;
const newConfig: BackendConfigType = {
// TODO: Make this work
/*const newConfig: BackendConfigType = {
...config,
widgets: [...config.widgets.filter((w) => w.id !== widget.id), widget],
};
const targetPath = path.join('data/configs', `${input.configName}.json`);
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');*/
}),
});

View File

@@ -6,7 +6,7 @@ import xss from 'xss';
import { z } from 'zod';
import { getConfig } from '~/tools/config/getConfig';
import { Stopwatch } from '~/tools/shared/time/stopwatch.tool';
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
import { IRssWidget, RssWidgetOptions } from '~/widgets/rss/RssWidgetTile';
import { createTRPCRouter, publicProcedure } from '../trpc';
@@ -58,11 +58,11 @@ export const rssRouter = createTRPCRouter({
.query(async ({ input }) => {
const config = getConfig(input.configName);
const rssWidget = config.widgets.find((x) => x.type === 'rss' && x.id === input.widgetId) as
| IRssWidget
| undefined;
const rssWidgetOptions = config.widgets.find(
(x) => x.type === 'rss' && x.id === input.widgetId
)?.properties as RssWidgetOptions | undefined;
if (!rssWidget) {
if (!rssWidgetOptions) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'required widget does not exist',
@@ -70,12 +70,12 @@ export const rssRouter = createTRPCRouter({
}
if (input.feedUrls.length === 0) {
return []
return [];
}
const result = await Promise.all(
input.feedUrls.map(async (feedUrl) =>
getFeedUrl(feedUrl, rssWidget.properties.dangerousAllowSanitizedItemContent)
getFeedUrl(feedUrl, rssWidgetOptions.dangerousAllowSanitizedItemContent)
)
);

View File

@@ -3,11 +3,11 @@ import {
Box,
Button,
Card,
Divider,
Flex,
Group,
Image,
ScrollArea,
Divider,
Stack,
Switch,
Text,
@@ -27,14 +27,14 @@ import {
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useEffect } from 'react';
import React 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';
import { defineWidget } from '../helper';
import { IDraggableEditableListInputValue, IWidget } from '../widgets';
import { IDraggableEditableListInputValue, IWidget, InferWidget } from '../widgets';
interface BookmarkItem {
id: string;
@@ -54,7 +54,7 @@ const definition = defineWidget({
type: 'text',
defaultValue: '',
info: true,
infoLink: "https://homarr.dev/docs/widgets/bookmarks/",
infoLink: 'https://homarr.dev/docs/widgets/bookmarks/',
},
items: {
type: 'draggable-editable-list',
@@ -84,11 +84,11 @@ const definition = defineWidget({
return undefined;
}
return t('item.validation.length', {shortest: "1", longest: "100"});
return t('item.validation.length', { shortest: '1', longest: '100' });
},
href: (value) => {
if (!z.string().min(1).max(200).safeParse(value).success) {
return t('item.validation.length', {shortest: "1", longest: "200"});
return t('item.validation.length', { shortest: '1', longest: '200' });
}
if (!z.string().url().safeParse(value).success) {
@@ -102,7 +102,7 @@ const definition = defineWidget({
return undefined;
}
return t('item.validation.length', {shortest: "1", longest: "400"});
return t('item.validation.length', { shortest: '1', longest: '400' });
},
},
validateInputOnChange: true,
@@ -174,11 +174,7 @@ const definition = defineWidget({
} satisfies IDraggableEditableListInputValue<BookmarkItem>,
layout: {
type: 'select',
data: [
{ value: 'autoGrid', },
{ value: 'horizontal', },
{ value: 'vertical', },
],
data: [{ value: 'autoGrid' }, { value: 'horizontal' }, { value: 'vertical' }],
defaultValue: 'autoGrid',
},
},
@@ -191,7 +187,7 @@ const definition = defineWidget({
component: BookmarkWidgetTile,
});
export type IBookmarkWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type IBookmarkWidget = InferWidget<typeof definition>;
interface BookmarkWidgetTileProps {
widget: IBookmarkWidget;
@@ -203,7 +199,7 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
const { fn, colors, colorScheme } = useMantineTheme();
const { t } = useTranslation('modules/bookmark');
if (widget.properties.items.length === 0) {
if (widget.options.items.length === 0) {
return (
<Stack align="center">
<IconPlaylistX />
@@ -219,17 +215,19 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
);
}
switch (widget.properties.layout) {
switch (widget.options.layout) {
case 'autoGrid':
return (
<Stack h="100%" spacing={0}>
<Title size="h4" px="0.25rem">{widget.properties.name}</Title>
<Title size="h4" px="0.25rem">
{widget.options.name}
</Title>
<Box
className={classes.grid}
mr={isEditModeEnabled && widget.properties.name === "" ? 'xl' : undefined}
mr={isEditModeEnabled && widget.options.name === '' ? 'xl' : undefined}
h="100%"
>
{widget.properties.items.map((item: BookmarkItem, index) => (
{widget.options.items.map((item: BookmarkItem, index) => (
<Card
className={classes.autoGridItem}
key={index}
@@ -239,10 +237,12 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
href={item.href}
target={item.openNewTab ? '_blank' : undefined}
withBorder
bg={colorScheme === 'dark' ? colors.dark[5].concat('80') : colors.blue[0].concat('80')}
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',
'&:hover': { backgroundColor: fn.primaryColor().concat('40') }, //'40' = 25% opacity
flex: '1 1 auto',
}}
display="flex"
>
@@ -257,38 +257,38 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
return (
<Stack h="100%" spacing={0}>
<Title size="h4" px="0.25rem">
{widget.properties.name}
{widget.options.name}
</Title>
<ScrollArea
scrollbarSize={8}
type="auto"
h="100%"
offsetScrollbars
mr={isEditModeEnabled && widget.properties.name === ""? 'xl' : undefined}
mr={isEditModeEnabled && widget.options.name === '' ? 'xl' : undefined}
styles={{
viewport:{
viewport: {
//mantine being mantine again... this might break. Needed for taking 100% of widget space
'& div[style="min-width: 100%; display: table;"]':{
'& div[style="min-width: 100%; display: table;"]': {
display: 'flex !important',
height:'100%',
height: '100%',
},
},
}}
>
<Flex
direction={ widget.properties.layout === 'vertical' ? 'column' : 'row' }
direction={widget.options.layout === 'vertical' ? 'column' : 'row'}
gap="0"
h="100%"
w="100%"
>
{widget.properties.items.map((item: BookmarkItem, index) => (
{widget.options.items.map((item: BookmarkItem, index) => (
<>
{index > 0 &&
{index > 0 && (
<Divider
m="3px"
orientation={ widget.properties.layout !== 'vertical' ? 'vertical' : 'horizontal' }
orientation={widget.options.layout !== 'vertical' ? 'vertical' : 'horizontal'}
/>
}
)}
<Card
key={index}
px="md"
@@ -299,13 +299,13 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
radius="md"
bg="transparent"
sx={{
'&:hover': { backgroundColor: fn.primaryColor().concat('40'),}, //'40' = 25% opacity
flex:'1 1 auto',
'&:hover': { backgroundColor: fn.primaryColor().concat('40') }, //'40' = 25% opacity
flex: '1 1 auto',
overflow: 'unset',
}}
display="flex"
>
<BookmarkItemContent item={item}/>
<BookmarkItemContent item={item} />
</Card>
</>
))}
@@ -321,26 +321,28 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
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 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>
)};
<Group spacing="0rem 1rem">
<Image
hidden={item.hideIcon}
src={item.iconUrl}
width={47}
height={47}
fit="contain"
withPlaceholder
/>
<Stack spacing={0}>
<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: {

View File

@@ -4,8 +4,9 @@ import { IconBrandGithub, IconBug, IconInfoCircle, IconRefresh } from '@tabler/i
import Consola from 'consola';
import { withTranslation } from 'next-i18next';
import React, { ReactNode } from 'react';
import { WidgetItem } from '~/components/Board/context';
import { WidgetsMenu } from '~/components/Dashboard/Tiles/Widgets/WidgetsMenu';
import { IWidget } from './widgets';
type ErrorBoundaryState = {
@@ -17,7 +18,7 @@ type ErrorBoundaryProps = {
t: (key: string) => string;
children: ReactNode;
integration: string;
widget: IWidget<string, any>;
widget: WidgetItem;
};
/**

View File

@@ -1,11 +1,4 @@
import {
Button,
Container,
Indicator,
IndicatorProps,
Popover,
useMantineTheme,
} from '@mantine/core';
import { Container, Indicator, IndicatorProps, Popover, useMantineTheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { MediaList } from './MediaList';

View File

@@ -9,7 +9,7 @@ import { getLanguageByCode } from '~/tools/language';
import { RouterOutputs, api } from '~/utils/api';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
import { CalendarDay } from './CalendarDay';
import { getBgColorByDateAndTheme } from './bg-calculator';
import { MediasType } from './type';
@@ -50,7 +50,7 @@ const definition = defineWidget({
component: CalendarTile,
});
export type ICalendarWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type ICalendarWidget = InferWidget<typeof definition>;
interface CalendarTileProps {
widget: ICalendarWidget;
@@ -74,8 +74,8 @@ function CalendarTile({ widget }: CalendarTileProps) {
month: month.getMonth() + 1,
year: month.getFullYear(),
options: {
useSonarrv4: widget.properties.useSonarrv4,
showUnmonitored: widget.properties.showUnmonitored,
useSonarrv4: widget.options.useSonarrv4,
showUnmonitored: widget.options.showUnmonitored,
},
},
{
@@ -91,10 +91,10 @@ function CalendarTile({ widget }: CalendarTileProps) {
defaultDate={new Date()}
onPreviousMonth={setMonth}
onNextMonth={setMonth}
size={widget.properties.fontSize}
size={widget.options.fontSize}
locale={language.locale}
firstDayOfWeek={getFirstDayOfWeek(firstDayOfWeek)}
hideWeekdays={widget.properties.hideWeekDays}
hideWeekdays={widget.options.hideWeekDays}
style={{ position: 'relative' }}
date={month}
maxLevel="month"
@@ -132,7 +132,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
flex: 1,
},
day: {
borderRadius: ['xs', 'sm'].includes(widget.properties.fontSize) ? radius.md : radius.lg,
borderRadius: ['xs', 'sm'].includes(widget.options.fontSize) ? radius.md : radius.lg,
},
}}
getDayProps={(date) => ({
@@ -142,7 +142,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
<CalendarDay
date={date}
medias={getReleasedMediasForDate(medias, date, widget)}
size={widget.properties.fontSize}
size={widget.options.fontSize}
/>
)}
/>
@@ -161,7 +161,7 @@ const getReleasedMediasForDate = (
date: Date,
widget: ICalendarWidget
): MediasType => {
const { radarrReleaseType } = widget.properties;
const { radarrReleaseType } = widget.options;
const books =
medias?.books.filter((b) => new Date(b.releaseDate).toDateString() === date.toDateString()) ??

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
import { DashDotGraph } from './DashDotGraph';
const definition = defineWidget({
@@ -144,7 +144,7 @@ const definition = defineWidget({
component: DashDotTile,
});
export type IDashDotTile = IWidget<(typeof definition)['id'], typeof definition>;
export type IDashDotTile = InferWidget<typeof definition>;
interface DashDotTileProps {
widget: IDashDotTile;
@@ -154,7 +154,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
const { classes } = useDashDotTileStyles();
const { t } = useTranslation('modules/dashdot');
const dashDotUrl = widget.properties.url;
const dashDotUrl = widget.options.url;
const locationProtocol = window.location.protocol;
const detectedProtocolDowngrade =
locationProtocol === 'https:' && dashDotUrl.toLowerCase().startsWith('http:');
@@ -178,7 +178,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
);
}
const { dashName, graphsOrder, usePercentages, columns, graphHeight } = widget.properties;
const { dashName, graphsOrder, usePercentages, columns, graphHeight } = widget.options;
return (
<Stack spacing="xs">

View File

@@ -14,12 +14,12 @@ import { useElementSize } from '@mantine/hooks';
import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { useConfigContext } from '~/config/provider';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
import { useDnsHoleSummeryQuery } from './DnsHoleSummary';
const definition = defineWidget({
@@ -40,7 +40,7 @@ const definition = defineWidget({
component: DnsHoleControlsWidgetTile,
});
export type IDnsHoleControlsWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type IDnsHoleControlsWidget = InferWidget<typeof definition>;
interface DnsHoleControlsWidgetProps {
widget: IDnsHoleControlsWidget;
@@ -111,7 +111,7 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
return (
<Stack justify="space-between" h={'100%'} spacing="0.25rem">
{sessionData?.user?.isAdmin && widget.properties.showToggleAllButtons && (
{sessionData?.user?.isAdmin && widget.options.showToggleAllButtons && (
<SimpleGrid
ref={ref}
cols={width > 275 ? 2 : 1}
@@ -120,15 +120,18 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
>
<Button
onClick={async () => {
await mutateAsync({
action: 'enable',
configName,
appsToChange: getDnsStatus()?.disabled,
},{
onSettled: () => {
reFetchSummaryDns();
await mutateAsync(
{
action: 'enable',
configName,
appsToChange: getDnsStatus()?.disabled,
},
{
onSettled: () => {
reFetchSummaryDns();
},
}
});
);
}}
disabled={getDnsStatus()?.disabled.length === 0 || fetchingDnsSummary || changingStatus}
leftIcon={<IconPlayerPlay size={20} />}
@@ -140,15 +143,18 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
</Button>
<Button
onClick={async () => {
await mutateAsync({
action: 'disable',
configName,
appsToChange: getDnsStatus()?.enabled,
},{
onSettled: () => {
reFetchSummaryDns();
await mutateAsync(
{
action: 'disable',
configName,
appsToChange: getDnsStatus()?.enabled,
},
{
onSettled: () => {
reFetchSummaryDns();
},
}
});
);
}}
disabled={getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus}
leftIcon={<IconPlayerStop size={20} />}
@@ -166,7 +172,7 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
display="flex"
style={{
flex: '1',
justifyContent: widget.properties.showToggleAllButtons ? 'flex-end' : 'space-evenly',
justifyContent: widget.options.showToggleAllButtons ? 'flex-end' : 'space-evenly',
}}
>
{data.status.map((dnsHole, index) => {

View File

@@ -10,12 +10,12 @@ import {
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { formatNumber, formatPercentage } from '~/tools/client/math';
import { RouterOutputs, api } from '~/utils/api';
import { formatNumber, formatPercentage } from '~/tools/client/math';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
const availableLayouts = ['grid', 'row', 'column'] as const;
type AvailableLayout = (typeof availableLayouts)[number];
@@ -43,7 +43,7 @@ const definition = defineWidget({
component: DnsHoleSummaryWidgetTile,
});
export type IDnsHoleSummaryWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type IDnsHoleSummaryWidget = InferWidget<typeof definition>;
interface DnsHoleSummaryWidgetProps {
widget: IDnsHoleSummaryWidget;
@@ -57,12 +57,12 @@ function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
}
return (
<Container h="100%" p={0} style={constructContainerStyle(widget.properties.layout)}>
<Container h="100%" p={0} style={constructContainerStyle(widget.options.layout)}>
{stats.map((item, index) => (
<StatCard
key={item.label ?? index}
item={item}
usePiHoleColors={widget.properties.usePiHoleColors}
usePiHoleColors={widget.options.usePiHoleColors}
data={data}
/>
))}

View File

@@ -11,21 +11,21 @@ import {
} from '@mantine/core';
import { useElementSize, useListState } from '@mantine/hooks';
import { linearGradientDef } from '@nivo/core';
import { Datum, ResponsiveLine, Serie } from '@nivo/line';
import { type Datum, ResponsiveLine, type Serie } from '@nivo/line';
import { IconDownload, IconUpload } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useEffect } from 'react';
import { AppAvatar } from '~/components/AppAvatar';
import { useConfigContext } from '~/config/provider';
import { useGetDownloadClientsQueue } from './useGetNetworkSpeed';
import { useColorTheme } from '~/tools/color';
import { humanFileSize } from '~/tools/humanFileSize';
import {
NormalizedDownloadQueueResponse,
TorrentTotalDownload,
} from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
import definition, { ITorrentNetworkTraffic } from './TorrentNetworkTrafficTile';
import { useGetDownloadClientsQueue } from './useGetNetworkSpeed';
interface TorrentNetworkTrafficTileProps {
widget: ITorrentNetworkTraffic;

View File

@@ -2,7 +2,7 @@ import { IconArrowsUpDown } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { InferWidget } from '../widgets';
const torrentNetworkTrafficTile = dynamic(() => import('./Tile'), {
ssr: false,
@@ -22,6 +22,6 @@ const definition = defineWidget({
component: torrentNetworkTrafficTile,
});
export type ITorrentNetworkTraffic = IWidget<(typeof definition)['id'], typeof definition>;
export type ITorrentNetworkTraffic = InferWidget<typeof definition>;
export default definition;

View File

@@ -3,7 +3,7 @@ import { IconBrowser, IconUnlink } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
const definition = defineWidget({
id: 'iframe',
@@ -55,7 +55,7 @@ const definition = defineWidget({
component: IFrameTile,
});
export type IIFrameWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type IIFrameWidget = InferWidget<typeof definition>;
interface IFrameTileProps {
widget: IIFrameWidget;
@@ -65,7 +65,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
const { t } = useTranslation('modules/iframe');
const { classes } = useStyles();
if (!widget.properties.embedUrl) {
if (!widget.options.embedUrl) {
return (
<Center h="100%">
<Stack align="center">
@@ -85,31 +85,31 @@ function IFrameTile({ widget }: IFrameTileProps) {
const allowedPermissions: string[] = [];
if (widget.properties.allowTransparency) {
if (widget.options.allowTransparency) {
allowedPermissions.push('transparency');
}
if (widget.properties.allowFullScreen) {
if (widget.options.allowFullScreen) {
allowedPermissions.push('fullscreen');
}
if (widget.properties.allowPayment) {
if (widget.options.allowPayment) {
allowedPermissions.push('payment');
}
if (widget.properties.allowAutoPlay) {
if (widget.options.allowAutoPlay) {
allowedPermissions.push('autoplay');
}
if (widget.properties.allowCamera) {
if (widget.options.allowCamera) {
allowedPermissions.push('camera');
}
if (widget.properties.allowMicrophone) {
if (widget.options.allowMicrophone) {
allowedPermissions.push('microphone');
}
if (widget.properties.allowGeolocation) {
if (widget.options.allowGeolocation) {
allowedPermissions.push('geolocation');
}
@@ -117,7 +117,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
<Container h="100%" w="100%" maw="initial" mah="initial" p={0}>
<iframe
className={classes.iframe}
src={widget.properties.embedUrl}
src={widget.options.embedUrl}
title="widget iframe"
allow={allowedPermissions.join(' ')}
>

View File

@@ -1,5 +1,6 @@
import {
ActionIcon, Anchor,
ActionIcon,
Anchor,
Badge,
Card,
Center,
@@ -9,7 +10,8 @@ import {
ScrollArea,
Stack,
Text,
Tooltip, useMantineTheme,
Tooltip,
useMantineTheme,
} from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconCheck, IconGitPullRequest, IconThumbDown, IconThumbUp } from '@tabler/icons-react';
@@ -20,7 +22,7 @@ import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { IWidget, InferWidget, InferWidgetOptions } from '../widgets';
import { useMediaRequestQuery } from './media-request-query';
import { MediaRequest, MediaRequestStatus } from './media-request-types';
@@ -46,7 +48,8 @@ const definition = defineWidget({
},
});
export type MediaRequestListWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type MediaRequestListWidget = InferWidget<typeof definition>;
export type MediaRequestListWidgetOptions = InferWidgetOptions<typeof definition>;
interface MediaRequestListWidgetProps {
widget: MediaRequestListWidget;
@@ -161,9 +164,9 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
</Group>
<Anchor
href={item.href}
target={widget.properties.openInNewTab ? "_blank" : "_self"}
target={widget.options.openInNewTab ? '_blank' : '_self'}
c={mantineTheme.colorScheme === 'dark' ? 'gray.3' : 'gray.8'}
>
>
{item.name}
</Anchor>
</Stack>
@@ -180,53 +183,54 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
/>
<Anchor
href={item.userLink}
target={widget.properties.openInNewTab ? "_blank" : "_self"}
target={widget.options.openInNewTab ? '_blank' : '_self'}
c={mantineTheme.colorScheme === 'dark' ? 'gray.3' : 'gray.8'}
>
{item.userName}
</Anchor>
</Flex>
{item.status === MediaRequestStatus.PendingApproval && sessionData?.user?.isAdmin && (
<Group>
<Tooltip label={t('tooltips.approve')} withArrow withinPortal>
<ActionIcon
variant="light"
color="green"
onClick={async () => {
notifications.show({
id: `approve ${item.id}`,
color: 'yellow',
title: t('tooltips.approving'),
message: undefined,
loading: true,
});
{item.status === MediaRequestStatus.PendingApproval &&
sessionData?.user?.isAdmin && (
<Group>
<Tooltip label={t('tooltips.approve')} withArrow withinPortal>
<ActionIcon
variant="light"
color="green"
onClick={async () => {
notifications.show({
id: `approve ${item.id}`,
color: 'yellow',
title: t('tooltips.approving'),
message: undefined,
loading: true,
});
await decideAsync({
request: item,
isApproved: true,
});
}}
>
<IconThumbUp />
</ActionIcon>
</Tooltip>
<Tooltip label={t('tooltips.decline')} withArrow withinPortal>
<ActionIcon
variant="light"
color="red"
onClick={async () => {
await decideAsync({
request: item,
isApproved: false,
});
}}
>
<IconThumbDown />
</ActionIcon>
</Tooltip>
</Group>
)}
await decideAsync({
request: item,
isApproved: true,
});
}}
>
<IconThumbUp />
</ActionIcon>
</Tooltip>
<Tooltip label={t('tooltips.decline')} withArrow withinPortal>
<ActionIcon
variant="light"
color="red"
onClick={async () => {
await decideAsync({
request: item,
isApproved: false,
});
}}
>
<IconThumbDown />
</ActionIcon>
</Tooltip>
</Group>
)}
</Stack>
</Flex>

View File

@@ -15,7 +15,7 @@ import { useTranslation } from 'next-i18next';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { InferWidget, InferWidgetOptions } from '../widgets';
import { useMediaRequestQuery, useUsersQuery } from './media-request-query';
import { MediaRequestStatus } from './media-request-types';
@@ -41,7 +41,8 @@ const definition = defineWidget({
component: MediaRequestStatsTile,
});
export type MediaRequestStatsWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type MediaRequestStatsWidget = InferWidget<typeof definition>;
export type MediaRequestStatsWidgetOptions = InferWidgetOptions<typeof definition>;
interface MediaRequestStatsWidgetProps {
widget: MediaRequestStatsWidget;
@@ -57,7 +58,7 @@ function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
const {
data: usersData,
isFetching: usersFetching,
isLoading: usersLoading
isLoading: usersLoading,
} = useUsersQuery(widget);
const { ref, height } = useElementSize();
const { colorScheme } = useMantineTheme();
@@ -128,7 +129,7 @@ function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
p={0}
component="a"
href={user.userLink}
target={widget.properties.openInNewTab ? "_blank" : "_self"}
target={widget.options.openInNewTab ? '_blank' : '_self'}
mah={95}
mih={55}
radius="md"

View File

@@ -1,22 +1,23 @@
import { useConfigContext } from '~/config/provider';
import { MediaRequestListWidget } from './MediaRequestListTile';
import { MediaRequestStatsWidget } from './MediaRequestStatsTile';
import { api } from '~/utils/api';
export const useMediaRequestQuery = (widget: MediaRequestListWidget|MediaRequestStatsWidget) => {
import { MediaRequestListWidget } from './MediaRequestListTile';
import { MediaRequestStatsWidget } from './MediaRequestStatsTile';
export const useMediaRequestQuery = (widget: MediaRequestListWidget | MediaRequestStatsWidget) => {
const { name: configName } = useConfigContext();
return api.mediaRequest.allMedia.useQuery(
{ configName: configName!, widget: widget },
{ configName: configName!, options: widget.options },
{
refetchInterval: 3 * 60 * 1000,
}
);
};
export const useUsersQuery = (widget: MediaRequestListWidget|MediaRequestStatsWidget) => {
export const useUsersQuery = (widget: MediaRequestListWidget | MediaRequestStatsWidget) => {
const { name: configName } = useConfigContext();
return api.mediaRequest.users.useQuery(
{ configName: configName!, widget: widget },
{ configName: configName!, options: widget.options },
{
refetchInterval: 3 * 60 * 1000,
}

View File

@@ -11,13 +11,13 @@ import {
} from '@mantine/core';
import { IconAlertTriangle, IconMovie } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { AppAvatar } from '~/components/AppAvatar';
import { useConfigContext } from '~/config/provider';
import { useGetMediaServers } from './useGetMediaServers';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
import { TableRow } from './TableRow';
import { useGetMediaServers } from './useGetMediaServers';
const definition = defineWidget({
id: 'media-server',
@@ -32,7 +32,7 @@ const definition = defineWidget({
},
});
export type MediaServerWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type MediaServerWidget = InferWidget<typeof definition>;
interface MediaServerWidgetProps {
widget: MediaServerWidget;

View File

@@ -22,7 +22,7 @@ import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget, InferWidgetOptions } from '../widgets';
const definition = defineWidget({
id: 'rss',
@@ -61,7 +61,8 @@ const definition = defineWidget({
component: RssTile,
});
export type IRssWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type IRssWidget = InferWidget<typeof definition>;
export type RssWidgetOptions = InferWidgetOptions<typeof definition>;
interface RssTileProps {
widget: IRssWidget;
@@ -72,8 +73,8 @@ function RssTile({ widget }: RssTileProps) {
const { name: configName } = useConfigContext();
const { data, isLoading, isFetching, isError, refetch } = useGetRssFeeds(
configName,
widget.properties.rssFeedUrl,
widget.properties.refreshInterval,
widget.options.rssFeedUrl,
widget.options.refreshInterval,
widget.id
);
const { classes } = useStyles();
@@ -164,7 +165,7 @@ function RssTile({ widget }: RssTileProps) {
className={classes.itemContent}
color="dimmed"
size="xs"
lineClamp={widget.properties.textLinesClamp}
lineClamp={widget.options.textLinesClamp}
dangerouslySetInnerHTML={{ __html: item.content }}
/>

View File

@@ -18,13 +18,13 @@ import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useTranslation } from 'next-i18next';
import { useCardStyles } from '~/components/layout/Common/useCardStyles';
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
import { NormalizedDownloadQueueResponse } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
import { AppIntegrationType } from '~/types/app';
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
import { BitTorrentQueueItem } from './TorrentQueueItem';
dayjs.extend(duration);
@@ -62,7 +62,7 @@ const definition = defineWidget({
component: TorrentTile,
});
export type ITorrent = IWidget<(typeof definition)['id'], typeof definition>;
export type ITorrent = InferWidget<typeof definition>;
interface TorrentTileProps {
widget: ITorrent;
@@ -193,15 +193,15 @@ function TorrentTile({ widget }: TorrentTileProps) {
export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
let result = torrents;
if (!widget.properties.displayCompletedTorrents) {
if (!widget.options.displayCompletedTorrents) {
result = result.filter((torrent) => !torrent.isCompleted);
}
if (widget.properties.labelFilter.length > 0) {
if (widget.options.labelFilter.length > 0) {
result = filterTorrentsByLabels(
result,
widget.properties.labelFilter,
widget.properties.labelFilterIsWhitelist
widget.options.labelFilter,
widget.options.labelFilterIsWhitelist
);
}
@@ -211,7 +211,7 @@ export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[])
};
const filterStaleTorrent = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
if (widget.properties.displayStaleTorrents) {
if (widget.options.displayStaleTorrents) {
return torrents;
}

View File

@@ -6,18 +6,18 @@ import duration from 'dayjs/plugin/duration';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import { useConfigContext } from '~/config/provider';
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
import { humanFileSize } from '~/tools/humanFileSize';
import { AppIntegrationType } from '~/types/app';
import {
useGetUsenetInfo,
usePauseUsenetQueueMutation,
useResumeUsenetQueueMutation,
} from '../dashDot/api';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { InferWidget } from '../widgets';
import { UsenetHistoryList } from './UsenetHistoryList';
import { UsenetQueueList } from './UsenetQueueList';
@@ -38,7 +38,7 @@ const definition = defineWidget({
},
});
export type IUsenetWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type IUsenetWidget = InferWidget<typeof definition>;
interface UseNetTileProps {
widget: IUsenetWidget;

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { IWidget, InferWidget } from '../widgets';
const VideoFeed = dynamic(() => import('./VideoFeed'), { ssr: false });
@@ -38,7 +38,7 @@ const definition = defineWidget({
component: VideoStreamWidget,
});
export type VideoStreamWidget = IWidget<(typeof definition)['id'], typeof definition>;
export type VideoStreamWidget = InferWidget<typeof definition>;
interface VideoStreamWidgetProps {
widget: VideoStreamWidget;
@@ -46,7 +46,7 @@ interface VideoStreamWidgetProps {
function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
const { t } = useTranslation('modules/video-stream');
if (!widget.properties.FeedUrl) {
if (!widget.options.FeedUrl) {
return (
<Center h="100%">
<Stack align="center">
@@ -59,10 +59,10 @@ function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
return (
<Group position="center" w="100%" h="100%">
<VideoFeed
source={widget?.properties.FeedUrl}
muted={widget?.properties.muted}
autoPlay={widget?.properties.autoPlay}
controls={widget?.properties.controls}
source={widget.options.FeedUrl}
muted={widget.options.muted}
autoPlay={widget.options.autoPlay}
controls={widget.options.controls}
/>
</Group>
);

View File

@@ -26,11 +26,13 @@ export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition>
};
export type InferWidget<TDefinition extends IWidgetDefinition> = WidgetItem & {
options: {
[key in keyof TDefinition['options']]: MakeLessSpecific<
TDefinition['options'][key]['defaultValue']
>;
};
options: InferWidgetOptions<TDefinition>;
};
export type InferWidgetOptions<TDefinition extends IWidgetDefinition> = {
[key in keyof TDefinition['options']]: MakeLessSpecific<
TDefinition['options'][key]['defaultValue']
>;
};
// Makes the type less specific