feat: add bookmarks options (apps) (#2048)
This commit is contained in:
@@ -2,8 +2,8 @@ import { objectEntries } from "@homarr/common";
|
|||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../../widgets/src/definition";
|
import type { WidgetComponentProps } from "../../../widgets/src/definition";
|
||||||
import { mapKind } from "./definitions";
|
|
||||||
import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
|
import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
|
||||||
|
import { mapKind } from "./definitions";
|
||||||
|
|
||||||
// This type enforces, that for all widget mappings there is a corresponding option mapping,
|
// This type enforces, that for all widget mappings there is a corresponding option mapping,
|
||||||
// each option of newmarr can be mapped from the value of the oldmarr options
|
// each option of newmarr can be mapped from the value of the oldmarr options
|
||||||
@@ -38,6 +38,9 @@ const optionMapping: OptionMapping = {
|
|||||||
|
|
||||||
return mappedLayouts[oldOptions.layout];
|
return mappedLayouts[oldOptions.layout];
|
||||||
},
|
},
|
||||||
|
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
|
||||||
|
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
|
||||||
|
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),
|
||||||
},
|
},
|
||||||
calendar: {
|
calendar: {
|
||||||
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
|
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
|
||||||
|
|||||||
@@ -1067,6 +1067,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hideIcon": {
|
||||||
|
"label": "Hide icons"
|
||||||
|
},
|
||||||
|
"hideHostname": {
|
||||||
|
"label": "Hide hostnames"
|
||||||
|
},
|
||||||
|
"openNewTab": {
|
||||||
|
"label": "Open in new tab"
|
||||||
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"label": "Bookmarks",
|
"label": "Bookmarks",
|
||||||
"add": "Add bookmark"
|
"add": "Add bookmark"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
import { Anchor, Box, Card, Divider, Flex, Group, Image, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -42,8 +42,25 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
|
|||||||
<Title order={4} px="0.25rem">
|
<Title order={4} px="0.25rem">
|
||||||
{options.title}
|
{options.title}
|
||||||
</Title>
|
</Title>
|
||||||
{options.layout === "grid" && <GridLayout data={data} width={width} height={height} />}
|
{options.layout === "grid" && (
|
||||||
{options.layout !== "grid" && <FlexLayout data={data} direction={options.layout} />}
|
<GridLayout
|
||||||
|
data={data}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
hideIcon={options.hideIcon}
|
||||||
|
hideHostname={options.hideHostname}
|
||||||
|
openNewTab={options.openNewTab}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{options.layout !== "grid" && (
|
||||||
|
<FlexLayout
|
||||||
|
data={data}
|
||||||
|
direction={options.layout}
|
||||||
|
hideIcon={options.hideIcon}
|
||||||
|
hideHostname={options.hideHostname}
|
||||||
|
openNewTab={options.openNewTab}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -51,9 +68,12 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
|
|||||||
interface FlexLayoutProps {
|
interface FlexLayoutProps {
|
||||||
data: RouterOutputs["app"]["byIds"];
|
data: RouterOutputs["app"]["byIds"];
|
||||||
direction: "row" | "column";
|
direction: "row" | "column";
|
||||||
|
hideIcon: boolean;
|
||||||
|
hideHostname: boolean;
|
||||||
|
openNewTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlexLayout = ({ data, direction }: FlexLayoutProps) => {
|
const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab }: FlexLayoutProps) => {
|
||||||
return (
|
return (
|
||||||
<Flex direction={direction} gap="0" h="100%" w="100%">
|
<Flex direction={direction} gap="0" h="100%" w="100%">
|
||||||
{data.map((app, index) => (
|
{data.map((app, index) => (
|
||||||
@@ -66,7 +86,7 @@ const FlexLayout = ({ data, direction }: FlexLayoutProps) => {
|
|||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
component="a"
|
component="a"
|
||||||
href={app.href ?? undefined}
|
href={app.href ?? undefined}
|
||||||
target="_blank"
|
target={openNewTab ? "_blank" : "_self"}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
key={app.id}
|
key={app.id}
|
||||||
h="100%"
|
h="100%"
|
||||||
@@ -81,7 +101,11 @@ const FlexLayout = ({ data, direction }: FlexLayoutProps) => {
|
|||||||
display="flex"
|
display="flex"
|
||||||
p={0}
|
p={0}
|
||||||
>
|
>
|
||||||
{direction === "row" ? <VerticalItem app={app} /> : <HorizontalItem app={app} />}
|
{direction === "row" ? (
|
||||||
|
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} />
|
||||||
|
) : (
|
||||||
|
<HorizontalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} />
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,9 +118,12 @@ interface GridLayoutProps {
|
|||||||
data: RouterOutputs["app"]["byIds"];
|
data: RouterOutputs["app"]["byIds"];
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
hideIcon: boolean;
|
||||||
|
hideHostname: boolean;
|
||||||
|
openNewTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GridLayout = ({ data, width, height }: GridLayoutProps) => {
|
const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab }: GridLayoutProps) => {
|
||||||
// Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items
|
// Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items
|
||||||
const columns = Math.ceil(Math.sqrt(data.length * (width / height)));
|
const columns = Math.ceil(Math.sqrt(data.length * (width / height)));
|
||||||
|
|
||||||
@@ -113,13 +140,13 @@ const GridLayout = ({ data, width, height }: GridLayoutProps) => {
|
|||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
component="a"
|
component="a"
|
||||||
href={app.href ?? undefined}
|
href={app.href ?? undefined}
|
||||||
target="_blank"
|
target={openNewTab ? "_blank" : "_self"}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
key={app.id}
|
key={app.id}
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
<Card withBorder style={{ containerType: "size" }} h="100%" className={classes.card} p="5cqmin">
|
<Card withBorder style={{ containerType: "size" }} h="100%" className={classes.card} p="5cqmin">
|
||||||
<VerticalItem app={app} />
|
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} />
|
||||||
</Card>
|
</Card>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
))}
|
))}
|
||||||
@@ -127,55 +154,79 @@ const GridLayout = ({ data, width, height }: GridLayoutProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const VerticalItem = ({ app }: { app: RouterOutputs["app"]["byIds"][number] }) => {
|
const VerticalItem = ({
|
||||||
|
app,
|
||||||
|
hideIcon,
|
||||||
|
hideHostname,
|
||||||
|
}: {
|
||||||
|
app: RouterOutputs["app"]["byIds"][number];
|
||||||
|
hideIcon: boolean;
|
||||||
|
hideHostname: boolean;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" gap="5cqmin">
|
<Stack h="100%" gap="5cqmin">
|
||||||
<Text fw={700} ta="center" size="20cqmin">
|
<Text fw={700} ta="center" size="20cqmin">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
<img
|
{!hideIcon && (
|
||||||
style={{
|
<Image
|
||||||
maxHeight: "100%",
|
style={{
|
||||||
maxWidth: "100%",
|
maxHeight: "100%",
|
||||||
overflow: "auto",
|
maxWidth: "100%",
|
||||||
flex: 1,
|
overflow: "auto",
|
||||||
objectFit: "contain",
|
flex: 1,
|
||||||
scale: 0.8,
|
objectFit: "contain",
|
||||||
}}
|
scale: 0.8,
|
||||||
src={app.iconUrl}
|
}}
|
||||||
alt={app.name}
|
src={app.iconUrl}
|
||||||
/>
|
alt={app.name}
|
||||||
<Anchor ta="center" component="span" size="12cqmin">
|
/>
|
||||||
{app.href ? new URL(app.href).hostname : undefined}
|
)}
|
||||||
</Anchor>
|
{!hideHostname && (
|
||||||
|
<Anchor ta="center" component="span" size="12cqmin">
|
||||||
|
{app.href ? new URL(app.href).hostname : undefined}
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HorizontalItem = ({ app }: { app: RouterOutputs["app"]["byIds"][number] }) => {
|
const HorizontalItem = ({
|
||||||
|
app,
|
||||||
|
hideIcon,
|
||||||
|
hideHostname,
|
||||||
|
}: {
|
||||||
|
app: RouterOutputs["app"]["byIds"][number];
|
||||||
|
hideIcon: boolean;
|
||||||
|
hideHostname: boolean;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
<img
|
{!hideIcon && (
|
||||||
style={{
|
<Image
|
||||||
overflow: "auto",
|
style={{
|
||||||
objectFit: "contain",
|
overflow: "auto",
|
||||||
scale: 0.8,
|
objectFit: "contain",
|
||||||
minHeight: "100cqh",
|
scale: 0.8,
|
||||||
maxHeight: "100cqh",
|
minHeight: "100cqh",
|
||||||
minWidth: "100cqh",
|
maxHeight: "100cqh",
|
||||||
maxWidth: "100cqh",
|
minWidth: "100cqh",
|
||||||
}}
|
maxWidth: "100cqh",
|
||||||
src={app.iconUrl}
|
}}
|
||||||
alt={app.name}
|
src={app.iconUrl}
|
||||||
/>
|
alt={app.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Stack justify="space-between" gap={0}>
|
<Stack justify="space-between" gap={0}>
|
||||||
<Text fw={700} size="45cqh" lineClamp={1}>
|
<Text fw={700} size="45cqh" lineClamp={1}>
|
||||||
{app.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Anchor component="span" size="30cqh">
|
{!hideHostname && (
|
||||||
{app.href ? new URL(app.href).hostname : undefined}
|
<Anchor component="span" size="30cqh">
|
||||||
</Anchor>
|
{app.href ? new URL(app.href).hostname : undefined}
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
|
|||||||
})),
|
})),
|
||||||
defaultValue: "column",
|
defaultValue: "column",
|
||||||
}),
|
}),
|
||||||
|
hideIcon: factory.switch({ defaultValue: false }),
|
||||||
|
hideHostname: factory.switch({ defaultValue: false }),
|
||||||
|
openNewTab: factory.switch({ defaultValue: true }),
|
||||||
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
|
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
|
||||||
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
|
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user