feat: add bookmarks options (apps) (#2048)

This commit is contained in:
Yossi Hillali
2025-01-23 22:10:16 +02:00
committed by GitHub
parent 860b73fa3f
commit 132db15424
4 changed files with 109 additions and 43 deletions

View File

@@ -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],

View File

@@ -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"

View File

@@ -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>
); );

View File

@@ -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 (