mirror of
https://github.com/ajnart/homarr.git
synced 2026-07-04 13:19:05 +02:00
✨ Add database board on overview, Migrate all widgets to new widget system, Improve gridstack, Migrate sidebar to new system
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"badges": {
|
||||
"fileSystem": "File system",
|
||||
"database": "Database",
|
||||
"default": "Default"
|
||||
}
|
||||
},
|
||||
|
||||
109
src/components/Board/item-actions.ts
Normal file
109
src/components/Board/item-actions.ts
Normal 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
|
||||
*/
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
|
||||
export type WidgetChangePositionModalInnerProps = {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
widget: IWidget<string, any>;
|
||||
widget: WidgetItem;
|
||||
wrapperColumnCount: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');*/
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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()) ??
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(' ')}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user